D3.js update graph when pushing new nodes - javascript

I am trying to add new nodes to a D3 graph dynamically by nodes.push({"id": item, "group": 1, "size": 30}); but when I do this there is a visual bug where there are duplicates. Anytime I update() I get a double of whatever was already there. Anyone have any advice? Would be appreciated.
var node;
var link;
var circles
var lables;
function update(){
node = svg.append("g")
.attr("class", "nodes")
.selectAll("g")
.data(nodes)
.enter().append("g")
link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
circles = node.append("circle")
.attr("r", function(d) { return (d.size / 10) + 1})
.attr("fill", function(d) { return color(3); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", clicked);
lables = node.append("text")
.text(function(d) {
return d.id;
})
.attr('x', 6)
.attr('y', 3)
.style("font-size", "20px");
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
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("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
}

Looking just at the nodes (the links are essentially the same issue), every time you update your data you:
Create a new parent g (svg.append("g"))
Select all the child g elements of that new parent g (.selectAll("g")). Since this new parent g has no children - you just made it, nothing is selected.
Bind data to the selection (.data(nodes))
Using the enter selection, append a g for each item in the data array (as there are no elements in the selection, everything is entered (the enter selection creates an element in the DOM for every item in the data array for which no corresponding element exists in the selection.)
Append a circle to each newly appended g. (.enter().append("g"))
Nowhere do you select the already existing nodes - these are just cast aside. They are ignored by the tick function because link and node refer to selections of newly created nodes and links. Neither do you remove the old links and nodes - so they just sit there for all eternity or until you close the browser.
The canonical solution is to:
Append structural elements once. I say structural in reference to the parent g elements: they aren't data dependent, they're organizational. They should be appended once outside of the update function.
Use the update function to manage (create, update, remove) elements that are dependent on the data: the nodes and links themselves. Anything that depends on the data needs to be modified in the update function, nothing else.
So we could append the parent g elements outside of the update function:
var nodeG = svg.append("g").attr("class", "nodes");
var linkG = svg.append("g").attr("class", "links");
Then in the update function we can use those selections to conduct the enter/update/exit cycle. This is slightly complicated in your case, and many others, because we have nodes represented by a g with child elements. Something like the following:
function update() {
var node = nodeG.selectAll("g")
.data(nodes)
// remove excess nodes.
node.exit().remove();
// enter new nodes as required:
var nodeEnter = node.enter().append("g")
.attr(...
// append circles to new nodes:
nodeEnter.append("circle")
.attr(...
// merge update and enter.
node = nodeEnter.merge(node);
// do enter/update/exit with lines.
var link = linkG.selectAll("line")
.attr(...
link.exit().remove();
var linkEnter = link.enter().append("line")
.attr(...
link = linkEnter.merge(link);
...
Which in your case may look like:
// Random data:
let graph = { nodes: [], links : [] }
function randomizeData(graph) {
// generate nodes:
let n = Math.floor(Math.random() * 10) + 6;
let newNodes = [];
for(let i = 0; i < n; i++) {
if (graph.nodes[i]) newNodes.push(graph.nodes[i]);
else newNodes.push({ id: i,
color: Math.floor(Math.random()*10),
size: Math.floor(Math.random() * 10 + 2),
x: (Math.random() * width),
y: (Math.random() * height)
})
}
// generate links
let newLinks = [];
let m = Math.floor(Math.random() * n) + 1;
for(let i = 0; i < m; i++) {
a = 0; b = 0;
while (a == b) {
a = Math.floor(Math.random() * n);
b = Math.floor(Math.random() * n);
}
newLinks.push({source: a, target: b})
if(i < newNodes.length - 2) newLinks.push({source: i, target: i+1})
}
return { nodes: newNodes, links: newLinks }
}
// On with main code:
// Set up the structure:
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
const color = d3.scaleOrdinal(d3.schemeCategory10);
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.004))
.force("charge", d3.forceManyBody())
// to attract nodes to center, use forceX and forceY:
.force("x", d3.forceX().x(width/2).strength(0.01))
.force("y", d3.forceY().y(height/2).strength(0.01));
const nodeG = svg.append("g").attr("class","nodes")
const linkG = svg.append("g").attr("class", "links")
graph = randomizeData(graph);
update();
// Two variables to hold our links and nodes - declared outside the update function so that the tick function can access them.
var links;
var nodes;
// Update based on data:
function update() {
// Select all nodes and bind data:
nodes = nodeG.selectAll("g")
.data(graph.nodes);
// Remove excess nodes:
nodes.exit()
.transition()
.attr("opacity",0)
.remove();
// Enter new nodes:
var newnodes = nodes.enter().append("g")
.attr("opacity", 0)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
// for effect:
newnodes.transition()
.attr("opacity",1)
.attr("class", "nodes")
newnodes.append("circle")
.attr("r", function(d) { return (d.size * 2) + 1})
.attr("fill", function(d) { return color(d.color); })
newnodes.append("text")
.text(function(d) { return d.id; })
.attr('x', 6)
.attr('y', 3)
.style("font-size", "20px");
newnodes.append("title")
.text(function(d) { return d.id; });
// Combine new nodes with old nodes:
nodes = newnodes.merge(nodes);
// Repeat but with links:
links = linkG.selectAll("line")
.data(graph.links)
// Remove excess links:
links.exit()
.transition()
.attr("opacity",0)
.remove();
// Add new links:
var newlinks = links.enter()
.append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
// for effect:
newlinks
.attr("opacity", 0)
.transition()
.attr("opacity",1)
// Combine new links with old:
links = newlinks.merge(links);
// Update the simualtion:
simulation
.nodes(graph.nodes) // the data array, not the selection of nodes.
.on("tick", ticked)
.force("link").links(graph.links)
simulation.alpha(1).restart();
}
function ticked() {
links // the selection of all links:
.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; });
nodes
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
}
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;
}
d3.select("button")
.on("click", function() {
graph = randomizeData(graph);
update();
})
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<button> Update</button>
<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
Note
I've updated the force paramaters a bit to use forceX and forceY: forces which attract the nodes to the center. The centering force only ensures the center of gravity is a specific value, not how close the nodes must be.
Alternative approaches:
Of course, you could just remove everything and append it each time: but this limits ability to transition from one dataset to the next and is generally not canonical.
If you only need to enter elements once (no elements need to be added or removed during updates) then you could avoid using the full enter/update/exit cycle and append once outside the update function, updating node/link attributes with new data on update rather than using the more involved enter/update/exit cycle in the snippet above.

Related

d3.js Molecule Diagram only working on the last element of the object

so I'm trying to create a visual representations of a couple of vlans and the connections of switches in each of them. I tried implementing it with this example I found online https://bl.ocks.org/mbostock/3037015 , the problem is that when i created a loop to go through all of the vlans, only the last vlan is drawn, there's really no reason I can see of why this is happening since all elements are calling the function.
If I remove the last element from the array with delete data['80'] then the one before the last starts working, so the only one working it the last one of the dictionary object, don't why though
code:
var data = {{ graph_vlans | safe }};
console.log(data);
$(document).ready(() => {
//-----------------------------------------------------------------
// TREE DISPLAY ---------------------------------------------------
//-----------------------------------------------------------------
var toggler = document.getElementsByClassName("caret");
for (var i = 0; i < toggler.length; i++) {
toggler[i].addEventListener("click", function () {
this.parentElement.querySelector(".nested").classList.toggle("active");
this.classList.toggle("caret-down");
});
}
//-----------------------------------------------------------------
// NETWORK DIAGRAM ------------------------------------------------
//-----------------------------------------------------------------
var width = 960, height = 500;
var color = d3.scale.category20();
var radius = d3.scale.sqrt().range([0, 6]);
var i = 0;
for (var key in data) {
console.log(key);
console.log(key["4"]);
var svg = d3.select("#graph_" + key).append("svg").attr("width", width).attr("height", height);
var force = d3.layout.force()
.size([width, height])
.charge(-400)
.linkDistance(function (d) {
return radius(d.source.size) + radius(d.target.size) + 20;
});
var graph = data[key];
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("g")
.attr("class", "link");
link.append("line")
.style("stroke-width", function (d) {
return (d.bond * 2 - 1) * 2 + "px";
});
link.filter(function (d) {
return d.bond > 1;
}).append("line")
.attr("class", "separator");
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", function (d) {
return radius(d.size);
})
.style("fill", function (d) {
return color(d.atom);
});
node.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.atom;
});
force.nodes(graph.nodes)
.links(graph.links)
.on("tick", tick)
.start();
i++;
}
function tick() {
link.selectAll("line")
.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 + ")";
});
}
});
Problem
I made some fake data for your plot and got this:
Your other force layouts are drawing, they're just not positioned. They're at [0,0] - barely visible here, in the top left corner of the SVG. So why is this?
Each for loop iteration you redefine any existing link and node variables - their scope extends beyond the for statement so you overwrite the previous defintion. var restricts a variables scope by function, the for statement doesn't limit scope if using var.
Because of this, when you call the tick function for each force layout, only the last layout is updated because node and link refer to the last layouts nodes and links.
So only your last force layout does anything.
Solution
There are a few solutions, I'm proposing one that adds two simple changes from your current code.
We need to get each force layout's nodes and links to the tick function. Currently we have all the force layout tick functions using the same node and link references. Ultimately, this is a variable scoping issue.
We can start by placing the tick function into the for loop. But, this still runs into the same problem by itself: node and link have a scope that isn't limited to the for loop (or the current iteration of the for loop) - each tick function will still use the same node and link references.
To fix this, we also need to use let when defining link and node (instead of var), now these variables have a block level scope, meaning each iteration's definitions of link and node won't overwrite the previous iterations.
By moving the tick function into the for loop and using let to define node and link, each time we call the tick function it will use the appropriate nodes and links.
Here's an example using a slightly modified example of the above code (removing some of the styling that relies on data properties and re-sizing the layouts for snippet view, but with the changes proposed above):
var data = {
"a":{
nodes:[{name:1},{name:2},{name:3}],
links:[
{source:1, target:2},
{source:2, target:0},
{source:0, target:1}
]
},
"b":{
nodes:[{name:"a"},{name:"b"},{name:"c"}],
links:[
{source:1, target:2},
{source:2, target:0},
{source:0, target:1}
]
}
}
// TREE DISPLAY
var width = 500, height = 100;
var color = d3.scale.category20();
var radius = d3.scale.sqrt().range([0, 6]);
var i = 0;
for (var key in data) {
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height);
var force = d3.layout.force()
.size([width, height])
.charge(-400)
.linkDistance(20);
var graph = data[key];
let link = svg.selectAll(".link")
.data(graph.links)
.enter().append("g")
.attr("class", "link");
link.append("line")
.style("stroke-width", 1)
.style("stroke","#ccc")
let node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node");
node.append("circle")
.attr("r", 5)
.attr("fill","#eee");
node.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.name;
});
force.nodes(graph.nodes)
.links(graph.links)
.on("tick", tick)
.start();
i++;
function tick() {
link.selectAll("line")
.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 + ")";
});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>

D3 updating graph with new elements create edges with the wrong nodes

My code creates a graph and creates a pivot point on each node, if you double click them it'll fetch more data associated with that node and hopefully creates new links. Now here's the problem I'm running into:
I clicked on one of the outermost nodes but for some reason the new links were being attached to the first node (the blue one). Any idea why this is happening?
function draw_graph(graph) {
var color = d3.scaleOrdinal(d3.schemeCategory20);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
node,
link;
svg.append('defs').append('marker')
.attrs({
'id': 'arrowhead',
'viewBox': '-0 -5 10 10',
'refX': 13,
'refY': 0,
'orient': 'auto',
'markerWidth': 13,
'markerHeight': 13,
'xoverflow': 'visible'
})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(200).strength(1))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
update(graph.links, graph.nodes);
svg.selectAll('circle').on('dblclick', function () {
var pivot_id = ($(this).siblings('title').text())
console.log('pivoting on', pivot_id)
pivot_search(pivot_id)
});
function update(links, nodes) {
link = svg.selectAll(".link")
.data(links)
.enter()
.append("line")
.attr("class", "link")
.attr('marker-end', 'url(#arrowhead)')
edgepaths = svg.selectAll(".edgepath")
.data(links)
.enter()
.append('path')
.attrs({
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'id': function (d, i) {
return 'edgepath' + i
}
})
.style("pointer-events", "none");
edgelabels = svg.selectAll(".edgelabel")
.data(links)
.enter()
.append('text')
.style("pointer-events", "none")
.attrs({
'class': 'edgelabel',
'id': function (d, i) {
return 'edgelabel' + i
},
'font-size': 10,
'fill': '#aaa'
});
node = svg.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
);
node.append("circle")
.attr("r", 5)
.attr("fill", function (d) {
return color(d.group);
})
node.append("title")
.text(function (d) {
return d.id;
});
node.append("text")
.attr("dy", -3)
.text(function (d) {
return d.label;
});
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
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("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")";
});
edgepaths.attr('d', function (d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
});
edgelabels.attr('transform', function (d) {
if (d.target.x < d.source.x) {
var bbox = this.getBBox();
rx = bbox.x + bbox.width / 2;
ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
}
else {
return 'rotate(0)';
}
});
}
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 pivot_search(entity_id) {
var json = {
'nodes': [],
'links': [],
}
get_entities({'id': entity_id})
.done(function (data) {
json.nodes.push({
'label': data['results'][0]['label'],
'id': data['results'][0]['id'],
'group': data['results'][0]['entity_type'],
})
get_entities({
'related_entities': entity_id,
'related_entities__entity_instance__entity_type__strong_entity': true,
'page_size': 500
})
.done(function (data) {
for (var i = 0; i < data['results'].length; i++) {
json.nodes.push({
'label': data['results'][i]['label'],
'id': data['results'][i]['id'],
'group': data['results'][i]['entity_type'],
})
json.links.push({
'source': entity_id,
'target': data['results'][i]['id'],
})
}
draw_graph(json)
})
})
}
EDIT: Upon further investigations seems like it's replacing the existing node links with the new data and creating new potentially duplicate nodes.
link = svg.selectAll('.link')
.data(links, function (d) {
return d.id;
})
.enter()
.append('line')
.attr('class', 'link')
.attr('marker-end', 'url(#arrowhead)')
edgepaths = svg.selectAll('.edgepath')
.data(links)
.enter()
.append('path')
.attrs({
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'id': function (d, i) {
return 'edgepath' + i
}
})
.style('pointer-events', 'none');
node = svg.selectAll('.node')
.data(nodes, function (d) {
return d.id;
})
.enter()
.append('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
);
I added an ID to help deal with node duplication, but now I have a problem with index root displacement.
It appears that the problem that you are having is due to the fact that your data might be duplicating within D3's data-join functionality. It is likely that the best way to solve the issue is to create a "UUID / GUID" for each node in your data before D3 binds it (see here for an example). Once you have done that, then you can bind the data and use the data-join's key-specify function (see here for an explanation) to tell D3 to use the UUID / GUID values you created for each object to guarantee consistency. From there, you should be able to handle the parent-child relationships easier.
Edit #1
Since that worked for the duplicate values, the next problem that you are likely having is because the reference to the "source" object is not set up the way that D3 would expect. In D3, the link's "source" property is a reference to the actual source object, where you are just providing the ID value (see here for the D3v4 docs reference). Try providing the reference to the actual source object within the array and that should fix it.
Edit #2
You are correct in that you are handling NEW data coming into the visualization, but I don't think that you're handling EXISTING or OLD (meaning, data points that are no longer relevant and the nodes / links need to be removed). In this case, try updating your code with the following example from Mike Bostock (the original creator of the D3.js library) here and then report back once that is done. It is possible that the new nodes that you are seeing are simply the older nodes that need to be removed since they no longer have the children tied to them, so D3js sees them as "new" or "existing" nodes that actually need to be removed.

JS- how to remove duplicate JSON nodes and add one link for all nodes that get merged

enter code here I have a JSON File from which I want to create a d3 directed
graph with arrows in the direction of higher influence score
{"nodes":[{"Name":"GJA","influenceScore":81.0,"type":10.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0},
{"Name":"SAG","influenceScore":89.0,"type":30.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0}],"links":
[{"source":0,"target":0,"type":"SA","value":1},
{"source":0,"target":1,"type":"SA","value":1},
{"source":0,"target":2,"type":"SA","value":1},
{"source":0,"target":3,"type":"SA","value":1}]}
I am a d3novice, so would like some help from experts here
My d3 code is here:
.link {
stroke: #ccc;
}
.node text {
pointer-events: none;
font: 12px sans-serif;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 1200,
height = 900;
var color = d3.scale.category10();
var fill = d3.scale.category10();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(0.052)
.distance(350)
.charge(-20)
.size([width, height]);
d3.json("\\abc.json", function(error, json) {
if (error) throw error;
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link").style("stroke-width", function(d) { return
Math.sqrt(d.value); }).style("stroke", function(d) {return
fill(d.value);});
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", "node")
.attr("r", function(d) { return (d.influenceScore/10) + 10;
}).style("fill", function(d) { return color(d.type); });
node.append("text")
.attr("dx", -35)
.attr("dy", "4.5em").text(function(d) { return d.Name });
node.append("title").text(function(d) { return d.Name ;});
force.on("tick", function() {
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 +
")"; });
});
});
I am getting the following image
I would like the target node e.g JJZ here just to occur once ( currently it's occurring as many number of times as it is repeated in the JSON i.e 2 times in the given example) however the line joining the two nodes should increase in thickness depending on the number of times the nodes repeat. so the blue line linking JJZ with GJA should be thicker than GJA and SAG and if another node occurs 5 times that should be thicker than JJZ and GJA. Also how do I insert directed arrows in the direction of a higher influence score
Your question here has little to do with D3: you can manipulate your array with plain JavaScript.
This function looks for the objects on json.nodes based on the property Name. If it doesn't exist, it pushes the object into an array that I named filtered. If it already exists, it increases the value of count in that object:
var filtered = []
json.nodes.forEach(function(d) {
if (!this[d.Name]) {
d.count = 0;
this[d.Name] = d;
filtered.push(this[d.Name])
}
this[d.Name].count += 1
}, Object.create(null))
Here is the demo:
var json = {"nodes":[{"Name":"GJA","influenceScore":81.0,"type":10.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0},
{"Name":"SAG","influenceScore":89.0,"type":30.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0}],"links":
[{"source":0,"target":0,"type":"SA","value":1},
{"source":0,"target":1,"type":"SA","value":1},
{"source":0,"target":2,"type":"SA","value":1},
{"source":0,"target":3,"type":"SA","value":1}]};
var filtered = []
json.nodes.forEach(function(d){
if(!this[d.Name]){
d.count = 0;
this[d.Name] = d;
filtered.push(this[d.Name])
}
this[d.Name].count += 1
}, Object.create(null))
console.log(filtered)
Then, you just need to use the property count to set the stroke-width of your links.

Reload d3.js graph on node click

I've had a d3 graph with a bunch of nodes based off items. When I click on one of those nodes, the graph is reloaded with data based off the clicked node.
I use a URL structure like so:
http://siteurl.com/index.html?item=
When a node is clicked, I have a function that runs the d3.json( function again with the new URL and then executes the update function again.
I've recently changed my code so that the node word appears below the node. Now I get an 'undefined is not a function' error on the line of code with node.exit().remove();
EDIT: Issue fixed from #Elijah's answer, but does not resolve my issue.
So when I click on a node, links get removed, then regenerated, but the nodes from the previous graph remain.
JSFiddle
Here's some of my JS
$wordToSearch = "bitter";
var w = 960,
h = 960,
node,
link,
root,
title;
var jsonURL = 'http://desolate-taiga-6759.herokuapp.com/word/' + $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)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 10)
.on("click", click)
.style("fill", "red");
node.append("text")
.attr("dy", 10 + 15)
.attr("text-anchor", "middle")
.text(function(d) { return d.word });
svg.selectAll(".node").data(nodes).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/' + $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];
}
}
}
Does anyone have any ideas why thats not working please?
EDIT: Updated my JS based from #Elijah's answer.
Handle the 3 states enter, exit and update, separate from each other:
node = svg.selectAll(".node")
.data(nodes); // base data selection, this is the update
var nodeE = node
.enter(); // handle the enter case
var nodeG = nodeE.append("g")
.attr("class", "node")
.call(force.drag); // add group ON ENTER
nodeG.append("circle")
.attr("r", 10)
.on("click", click)
.style("fill", "red"); // append circle to group ON ENTER
nodeG.append("text")
.attr("dy", 10 + 15)
.attr("text-anchor", "middle")
.text(function(d) { return d.word }); // append text to group ON ENTER
node.exit().remove(); // handle exit
Update fiddle here.
Your problem is that here:
node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
You're defining node as svg.selectAll(".node").enter() which means your variable now refers to the selection enter behavior and not the selection itself. So when you try to change exit behavior on it with: node.exit().remove();
..you're trying to access the .exit() behavior not of the selection but of the selection's .enter() behavior. Replace that with:
svg.selectAll(".node").data(nodes).exit().remove();
And that should fix your problem. There may be something else going on, but that's definitely going to cause issues.
Edited to add:
You should also update your tick function so that it doesn't reference node which is now assigned to the #selection.enter() and not the selection and instead reference the selection:
svg.selectAll("g.node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

Adding new nodes to Force-directed layout

First question on Stack Overflow, so bear with me! I am new to d3.js, but have been consistently amazed by what others are able to accomplish with it... and almost as amazed by how little headway I've been able to make with it myself! Clearly I'm not grokking something, so I hope that the kind souls here can show me the light.
My intention is to make a reusable javascript function which simply does the following:
Creates a blank force-directed graph in a specified DOM element
Allows you to add and delete labeled, image-bearing nodes to that graph, specifying connections between them
I've taken http://bl.ocks.org/950642 as a starting point, since that's essentially the kind of layout I want to be able to create:
Here's what my code looks like:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="underscore-min.js"></script>
<script type="text/javascript" src="d3.v2.min.js"></script>
<style type="text/css">
.link { stroke: #ccc; }
.nodetext { pointer-events: none; font: 10px sans-serif; }
body { width:100%; height:100%; margin:none; padding:none; }
#graph { width:500px;height:500px; border:3px solid black;border-radius:12px; margin:auto; }
</style>
</head>
<body>
<div id="graph"></div>
</body>
<script type="text/javascript">
function myGraph(el) {
// Initialise the graph object
var graph = this.graph = {
"nodes":[{"name":"Cause"},{"name":"Effect"}],
"links":[{"source":0,"target":1}]
};
// Add and remove elements on the graph object
this.addNode = function (name) {
graph["nodes"].push({"name":name});
update();
}
this.removeNode = function (name) {
graph["nodes"] = _.filter(graph["nodes"], function(node) {return (node["name"] != name)});
graph["links"] = _.filter(graph["links"], function(link) {return ((link["source"]["name"] != name)&&(link["target"]["name"] != name))});
update();
}
var findNode = function (name) {
for (var i in graph["nodes"]) if (graph["nodes"][i]["name"] === name) return graph["nodes"][i];
}
this.addLink = function (source, target) {
graph["links"].push({"source":findNode(source),"target":findNode(target)});
update();
}
// set up the D3 visualisation in the specified element
var w = $(el).innerWidth(),
h = $(el).innerHeight();
var vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.nodes(graph.nodes)
.links(graph.links)
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var update = function () {
var link = vis.selectAll("line.link")
.data(graph.links);
link.enter().insert("line")
.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; });
link.exit().remove();
var node = vis.selectAll("g.node")
.data(graph.nodes);
node.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("image")
.attr("class", "circle")
.attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "16px")
.attr("height", "16px");
node.append("text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
node.exit().remove();
force.on("tick", function() {
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 + ")"; });
});
// Restart the force layout.
force
.nodes(graph.nodes)
.links(graph.links)
.start();
}
// Make it all go
update();
}
graph = new myGraph("#graph");
// These are the sort of commands I want to be able to give the object.
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");
</script>
</html>
Every time I add a new node, it re-labels all of the existing nodes; these pile on top of each other and things start to get ugly. I understand why this is: because when I call the update() function function upon adding a new node, it does a node.append(...) to the entire data set. I can't figure out how to do this for only the node I'm adding... and I can only apparently use node.enter() to create a single new element, so that doesn't work for the additional elements I need bound to the node. How can I fix this?
Thank you for any guidance that you're able to give on any of this issue!
Edited because I quickly fixed a source of several other bugs that were previously mentioned
After many long hours of being unable to get this working, I finally stumbled across a demo that I don't think is linked any of the documentation: http://bl.ocks.org/1095795:
This demo contained the keys which finally helped me crack the problem.
Adding multiple objects on an enter() can be done by assigning the enter() to a variable, and then appending to that. This makes sense. The second critical part is that the node and link arrays must be based on the force() -- otherwise the graph and model will go out of synch as nodes are deleted and added.
This is because if a new array is constructed instead, it will lack the following attributes:
index - the zero-based index of the node within the nodes array.
x - the x-coordinate of the current node position.
y - the y-coordinate of the current node position.
px - the x-coordinate of the previous node position.
py - the y-coordinate of the previous node position.
fixed - a boolean indicating whether node position is locked.
weight - the node weight; the number of associated links.
These attributes are not strictly needed for the call to force.nodes(), but if these are not present, then they would be randomly initialised by force.start() on the first call.
If anybody is curious, the working code looks like this:
<script type="text/javascript">
function myGraph(el) {
// Add and remove elements on the graph object
this.addNode = function (id) {
nodes.push({"id":id});
update();
}
this.removeNode = function (id) {
var i = 0;
var n = findNode(id);
while (i < links.length) {
if ((links[i]['source'] === n)||(links[i]['target'] == n)) links.splice(i,1);
else i++;
}
var index = findNodeIndex(id);
if(index !== undefined) {
nodes.splice(index, 1);
update();
}
}
this.addLink = function (sourceId, targetId) {
var sourceNode = findNode(sourceId);
var targetNode = findNode(targetId);
if((sourceNode !== undefined) && (targetNode !== undefined)) {
links.push({"source": sourceNode, "target": targetNode});
update();
}
}
var findNode = function (id) {
for (var i=0; i < nodes.length; i++) {
if (nodes[i].id === id)
return nodes[i]
};
}
var findNodeIndex = function (id) {
for (var i=0; i < nodes.length; i++) {
if (nodes[i].id === id)
return i
};
}
// set up the D3 visualisation in the specified element
var w = $(el).innerWidth(),
h = $(el).innerHeight();
var vis = this.vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var nodes = force.nodes(),
links = force.links();
var update = function () {
var link = vis.selectAll("line.link")
.data(links, function(d) { return d.source.id + "-" + d.target.id; });
link.enter().insert("line")
.attr("class", "link");
link.exit().remove();
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id;});
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("image")
.attr("class", "circle")
.attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "16px")
.attr("height", "16px");
nodeEnter.append("text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {return d.id});
node.exit().remove();
force.on("tick", function() {
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 + ")"; });
});
// Restart the force layout.
force.start();
}
// Make it all go
update();
}
graph = new myGraph("#graph");
// You can do this from the console as much as you like...
graph.addNode("Cause");
graph.addNode("Effect");
graph.addLink("Cause", "Effect");
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");
</script>

Categories