d3 v4 adding new nodes to force directed graph - javascript
I'm new to d3.js and I'm currently stuck on a problem. I'm using the force directed graph to show relationships of my data. This should allow a user to add a new node to the existing graph and draw a relationship link between 2 or more nodes. My caveat is my data is being populated from an ajax call which I assign to a variable and pass that to a function which generates the graph. The initial load of the data works great and everything is displayed properly. My issue is when the user clicks a button to add the new node. On that action I am making a ajax call to retrieve a new un-linked relationship to add to the graph. I add the new retrieved data to the nodes array and attempt to re-draw the entire graph. However I receive errors on x & y attributes being set to NaN. I believe this is related to how forceSimulation assigns those values. I did attempt to use simulation.reset(), but it was not successful.
Here is some of my code;
Initial call to retrieve all existing relationships.
function getGraphData(){
$.ajax({
url: [link to rest uri],
type: 'GET',
contentType: 'application/json'
}).done(function(response){
drawGraph(response);
})
};
This is my second call to retrieve a new un-linked relationship
function getNewRelationshipData(){
$.ajax({
url: [link to second rest uri],
type: 'GET'
contentType: 'application/json'
}).done(function(response){
var newNode = response.nodes;
updateGraph();
//---same as getGraphData()
$.ajax({
url: [link to rest uri],
type: 'GET',
contentType: 'application/json'
}).done(function(response){
var graphData = response;
graphData.nodes[graphData.nodes.length] = newNode[0]
//assigned relationship data to graphData and appended the newNode value
drawGraph(graphData);
})
});
};
function updateGraph(){
// clears out old graph
d3.selectAll("svg > *").remove();
};
This is how I'm setting up my graph.
function drawGraph(relationships){
var svg = d3.select("svg"),
w = +svg.attr("width"),
h = +svg.attr("height);
var g = svg.append("g");
var color = d3.scaleOrdinal(d3.schemeCategory20);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(60))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(w / 2, h / 2))
.force("attraceForce",d3.forceManyBody().strength(-900));
var opacity = 0.05;
var transitionPeriod = 500;
var graph = relationships;
var link = g.selectAll("line")
.data(graph.links)
.enter().append("line")
.style("stroke-width", function(d) { return d.value; })
.style("stroke", "#999" )
.style("opacity", "1")
.attr("group",function(d) {return d.group; })
.on("click", function(d) {
// This is to toggle visibility - need to do it on the nodes, links & text
d3.selectAll("line:not([group='"+d.group+"'])")
.transition().duration(transitionPeriod).style("opacity", function() {
var currentDisplay = d3.select(this).style("opacity");
currentDisplay = currentDisplay == "1" ? opacity : "1";
return currentDisplay;
});
d3.selectAll("circle:not([group='"+d.group+"'])")
.transition().duration(transitionPeriod).style("opacity",function() {
var currentDisplay = d3.select(this).style("opacity");
currentDisplay = currentDisplay == "1" ? opacity : "1";
return currentDisplay;
});
d3.selectAll("text:not([group='"+d.group+"'])")
.transition().duration(transitionPeriod).style("opacity",function() {
var currentDisplay = d3.select(this).style("opacity");
currentDisplay = currentDisplay == "1" ? opacity : "1";
return currentDisplay;
});
})
var node = g
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 14)
.attr("fill", function(d) { return color(d.group); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
var images = g.selectAll("image")
.data(graph.nodes)
.enter().append("image")
.attr("xlink:href",function(d){
var type = d.type,
typeIcon = "",
switch(type){
//assigns an image based on the subject type person, address, phone, ect.
}
return typeIcon;
})
// This is the label for each node
var text = g.selectAll("text")
.data(graph.nodes)
.enter().append("text")
.attr("dx",12)
.attr("dy",".35m")
.text(function(d) { return d.id;})
.attr("text-anchor", "middle")
.attr("group",function(d) {return d.group;} ) ;
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
text
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
}
});
//Used to drag the graph round the screen
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;
}
// This is the zoom handler
var zoom_handler = d3.zoom()
.scaleExtent([1/4, 4])
.on("zoom", zoom_actions);
//specify what to do when zoom event listener is triggered
function zoom_actions(){
g.attr("transform", d3.event.transform);
}
// initial scaling on the svg container - this means everything in it is scaled as well
svg.call(zoom_handler)
.call(zoom_handler.transform, d3.zoomIdentity.scale(0.9,0.9))
;
zoom_handler(svg);
};
And my ajax data looks like this
{
"nodes":[
{"id": "1", "group": "1", "type": "person", "name":"Jon Doe"},
{"id": "2", "group": "1", "type": "person", "name":"Jane Doe"}
//ect list of ~50
],
"links":[
{"source": "1", "target":"2"},
//ect list of ~50
]
}
I hope someone with more d3.js experience can point me in the right direction.
I'm posting this there in case someone else had the same problem. I solved my problem by breaking up the drawGraph function into smaller widgets.
I moved the following to the parent scope.
var svg = d3.select("svg"),
w = +svg.attr("width"),
h = +svg.attr("height),
node,
link;
var simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(60))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(w / 2, h / 2))
.force("attraceForce",d3.forceManyBody().strength(-900));
var color = d3.scaleOrdinal(d3.schemeCategory20);
Then within the drawGraph function I made the following changes.
function drawGraph(nodes,links){
var g = svg.append("g");
link = g.selectAll(".link").data(links,function(d){ return d.target.id; })
link = link.enter()
.append("line")
.attr("class","link")
.style("stroke-width", function(d) { return d.value; })
.style("stroke", "#999")
node = g.selectAll(".node").data(nodes,function(d){ return d.id; })
node = node.enter()
.append("g")
.attr("class","node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
node.append("circle").attr("r", 14).attr("fill",function(d){return color(d.group);})
node.append("text").attr("dy", -15)
.text(function(d){ return d.id; })
.attr("text-anchor","middle")
.style("fill", "#555");
node.append("image")
.attr("xlink:href",function(d){
var type = d.type,
typeIcon = "",
switch(type){
//assigns an image based on the subject type person, address, phone, ect.
}
return typeIcon;
})
.attr("x", -8)
.attr("y", -8)
.attr("height", 16)
.attr("width", 16);
simulation.nodes(nodes).on("tick", ticked);
simulation.force("link").links(links);
function zoom_actions(){
g.attr("transform", d3.event.transform);
};
var zoom_handler = d3.zoom()
.scaleExtent([1/4, 4])
.on("zoom", zoom_actions);
svg.call(zoom_handler).call(zoom_handler.transform, d3.zoomIdentity.scale(0.9,0.9));
zoom_handler(svg);
};
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 + ")";
});
};
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;
}
I then added the following function which is used to setup the data and draw the graph.
function formatGraphData(relationships){
nodes = relationships.nodes;
links = relationships.links;
simulation.alpha(0.5).restart(); //<- needed to restart simulation and position nodes
drawGraph(nodes,links);
}
Then the ajax calls were updated to use formatGraphData instead of drawGraph.
I added the following to my css file
.links line{
stroke: #999;
}
.nodes circle{
stroke: #fff;
stroke-width: 1.5px;
}
Related
D3 nodes floating out of the frame
I got several nodes and links in place. Unfortunately those are "floating" out of the canvas. I am using D3.V4.js and found several guides how to solve the problem with D3.v3.js. Unfortuantely those doesn´t seem to work. Ideally a hidden or transparent frame would be arranged around the canvas area. I am new building D3 graphs, so I couldn´t figure it out yet. Maybe you guys could help me to adjust the correct line in my code. Thanks var svg = d3.select("svg"), width = window.innerWidth, height = +svg.attr("height"); var color = d3.scaleOrdinal(d3.schemeCategory20); var simulation = d3.forceSimulation() .force("link", d3.forceLink().id(function(d) { return d.id; }).distance(100)) .force("charge", d3.forceManyBody()) .force("center", d3.forceCenter(width / 2, height / 2)) .force("attraceForce",d3.forceManyBody().strength(-2)); var opacity = 0.25; d3.json("datav2.json", function(error, graph) { if (error) throw error; var link = svg.append("g") .attr("class", "links") .selectAll("line") .data(graph.links) .enter().append("line") .style("stroke-width", 3) .style("stroke-linecap", "round") .attr("linkGroup",function(d) {return d.linkGroup; }) .attr("stroke-width", function(d) { return d.value; }) ; var node = svg.append("g") .attr("class", "nodes") .selectAll("circle") .data(graph.nodes) .enter().append("circle") .attr("r", 15) .attr("fill", "#ffffff") .style("stroke-width", 2) .style("stroke", function(d) { return color(d.group); }) .attr("nodeGroup",function(d) {return d.nodeGroup; }) .on("click", function(d) { // This is to toggle visibility - need to do it on the nodes and links d3.selectAll("line:not([linkGroup='"+d.nodeGroup+"'])") .style("opacity", function() { currentDisplay = d3.select(this).style("opacity"); currentDisplay = currentDisplay == "1" ? "0.1" : "1"; return currentDisplay; }); d3.selectAll("circle:not([nodeGroup='"+d.nodeGroup+"'])") .style("opacity",function() { currentDisplay = d3.select(this).style("opacity"); currentDisplay = currentDisplay == "1" ? "0.1" : "1"; return currentDisplay; }); d3.selectAll("text:not([nodeGroup='"+d.nodeGroup+"'])") .style("opacity",function() { currentDisplay = d3.select(this).style("opacity"); currentDisplay = currentDisplay == "1" ? "0.1" : "1"; return currentDisplay; }); }) .on("mouseover", function(d) { d3.select(this).style("cursor", "crosshair"); }) .on("mouseout", function(d) { d3.select(this).style("cursor", "default"); }) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended)); // This is the label for each node var text = svg.append("g").selectAll("text") .data(graph.nodes) .enter().append("text") .attr("dy",-25) .text(function(d) { return d.name;}) .attr("text-anchor", "middle") .attr("nodeGroup",function(d) {return d.nodeGroup;} ) ; node.append("title") .text(function(d) { return d.name; }); simulation .nodes(graph.nodes) .on("tick", ticked); simulation.force("link") .links(graph.links); function neighboring(a, b) { return graph.links.some(function(d) { return (d.source.id === a.source.id && d.target.id === b.target.id) || (d.source.id === b.source.id && d.target.id === a.target.id); }); } 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; }) //.attr("cx2", function(d) { return d.x = Math.max(d.width, Math.min(width - d.width, d.x)); }) //.attr("cy2", function(d) { return d.y = Math.max(d.height, Math.min(height - heightDelta - d.height, d.y)); }); text .attr("x", function(d) { return d.x; }) .attr("y", function(d) { return 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; }
Ok I found the issue and added a radius var with a size which fits to the nodes and modified the following line: node .attr("cx", function(d) { return d.x = Math.max(radius, Math.min(width - radius, d.x)); }) .attr("cy", function(d) { return d.y = Math.max(radius, Math.min(height - radius, d.y)); })
How to make the extent of a brush match the size of the parent element?
I am trying to combine d3-brush with d3-zoom in a force-directed graph. Although I managed to combine the modules, it seems that I am not able to use the brush on the whole g element which contains the nodes.Thus, I am not able to select all the nodes in the graph. I have already had a look at some blocks, e.g. D3 Selectable Force-Directed Graph,https://bl.ocks.org/mbostock/4566102, D3v4 Selectable, Draggable, Zoomable Force Directed Graph which try to combine the two modules [1, 3] or use the brush event in a force-directed graph [2]. Moreover, I have also read the d3-brush API. However, I can not achieve the desired behavior. The user can select a region of points in the graph by holding the ctrl key and pressing the left mouse button (ctrl key + lmb). Theoretically, the user should be able to select all the nodes within the g element which contains the nodes. In the next figure you can see the maximum brush extent. Nodes outside the brush extent can not be selected. In the figure below, you can see the DOM hierarchy of the HTML document. I am suspecting that the extent of the brush is not defined correctly, which causes this issue.If I apply the brush to the svgForce element and the nodes are also drawn directly on the svgForce element, then the brush extent matches the dimensions of the svg. However, in that case, the zoom does not work properly, because it is not bound to a g element. svgForce.append("g") .attr("class", "brush") .call(d3.brush().on("brush", brushed)); Do you have any ideas what might be going wrong? var width_network = 700, height_network = 700, gForce, gMain, zoom, brush, CIRCLE_RADIUS = 10, link, node, nodeLabel, gBrush; var network = {"nodes":[{"name":"A"},{"name":"B"},{"name":"C"},{"name":"D"},{"name":"E"},{"name":"F"},{"name":"G"},{"name":"H"},{"name":"I"},{"name":"J"},{"name":"K"},{"name":"L"},{"name":"M"},{"name":"N"},{"name":"O"},{"name":"P"},{"name":"Q"},{"name":"R"},{"name":"S"},{"name":"T"},{"name":"U"},{"name":"V"},{"name":"W"},{"name":"X"},{"name":"Y"},{"name":"Z"},{"name":"AA"},{"name":"BB"},{"name":"CC"},{"name":"DD"},{"name":"EE"},{"name":"FF"},{"name":"GG"},{"name":"HH"},{"name":"II"},{"name":"JJ"},{"name":"KK"},{"name":"LL"},{"name":"MM"},{"name":"NN"},{"name":"OO"},{"name":"PP"},{"name":"QQ"},{"name":"RR"},{"name":"SS"},{"name":"TT"},{"name":"UU"},{"name":"VV"},{"name":"WW"},{"name":"XX"},{"name":"YY"},{"name":"ZZ"},{"name":"AAA"},{"name":"BBB"},{"name":"CCC"},{"name":"DDD"},{"name":"EEE"},{"name":"FFF"},{"name":"GGG"},{"name":"HHH"},{"name":"III"},{"name":"JJJ"},{"name":"KKK"},{"name":"LLL"},{"name":"MMM"},{"name":"NNN"},{"name":"OOO"},{"name":"PPP"},{"name":"QQQ"},{"name":"RRR"},{"name":"SSS"},{"name":"TTT"},{"name":"UUU"},{"name":"VVV"},{"name":"WWW"},{"name":"XXX"},{"name":"YYY"},{"name":"ZZZ"},{"name":"AAAA"},{"name":"BBBB"},{"name":"CCCC"},{"name":"DDDD"},{"name":"EEEE"},{"name":"FFFF"},{"name":"GGGG"},{"name":"HHHH"},{"name":"IIII"},{"name":"JJJJ"},{"name":"KKKK"},{"name":"LLLL"},{"name":"MMMM"},{"name":"NNNN"},{"name":"OOOO"},{"name":"PPPP"},{"name":"QQQQ"},{"name":"RRRR"},{"name":"SSSS"},{"name":"TTTT"},{"name":"UUUU"},{"name":"VVVV"},{"name":"WWWW"},{"name":"XXXX"},{"name":"YYYY"},{"name":"ZZZZ"},{"name":"A1"},{"name":"B2"},{"name":"C3"},{"name":"D4"},{"name":"E5"},{"name":"F6"},{"name":"G7"},{"name":"H8"},{"name":"I9"},{"name":"J10"},{"name":"K11"},{"name":"L12"},{"name":"M13"},{"name":"N14"},{"name":"O15"},{"name":"P16"},{"name":"Q17"},{"name":"R18"},{"name":"S19"},{"name":"T20"},{"name":"U21"},{"name":"V22"},{"name":"W23"},{"name":"X24"},{"name":"Y25"},{"name":"Z26"},{"name":"A27"},{"name":"B28"},{"name":"C29"},{"name":"D30"},{"name":"E31"},{"name":"F32"},{"name":"G33"},{"name":"H34"},{"name":"I35"},{"name":"J36"},{"name":"K37"},{"name":"L38"},{"name":"M39"},{"name":"N40"},{"name":"O41"},{"name":"P42"},{"name":"Q43"},{"name":"R44"},{"name":"S45"},{"name":"T46"},{"name":"U47"},{"name":"V48"},{"name":"W49"},{"name":"X50"},{"name":"Y51"},{"name":"Z52"},{"name":"A53"},{"name":"B54"},{"name":"C55"},{"name":"D56"},{"name":"E57"},{"name":"F58"},{"name":"G59"},{"name":"H60"},{"name":"I61"},{"name":"J62"},{"name":"K63"},{"name":"L64"},{"name":"M65"},{"name":"N66"},{"name":"O67"},{"name":"P68"},{"name":"Q69"},{"name":"R70"},{"name":"S71"},{"name":"T72"},{"name":"U73"},{"name":"V74"},{"name":"W75"},{"name":"X76"},{"name":"Y77"},{"name":"Z78"},{"name":"A79"},{"name":"B80"},{"name":"C81"},{"name":"D82"},{"name":"E83"},{"name":"F84"},{"name":"G85"},{"name":"H86"},{"name":"I87"},{"name":"J88"},{"name":"K89"},{"name":"L90"},{"name":"M91"},{"name":"N92"},{"name":"O93"},{"name":"P94"},{"name":"Q95"},{"name":"R96"},{"name":"S97"},{"name":"T98"},{"name":"U99"},{"name":"V100"},{"name":"W101"},{"name":"X102"},{"name":"Y103"},{"name":"Z104"},{"name":"A105"},{"name":"B106"},{"name":"C107"},{"name":"D108"},{"name":"E109"},{"name":"F110"},{"name":"G112"},{"name":"H113"},{"name":"I114"},{"name":"J115"},{"name":"K116"},{"name":"L117"},{"name":"M118"},{"name":"N119"},{"name":"O120"},{"name":"P121"},{"name":"Q123"},{"name":"R124"},{"name":"S125"},{"name":"T126"},{"name":"U127"},{"name":"V128"},{"name":"W129"},{"name":"X130"},{"name":"Y131"},{"name":"Z134"},{"name":"A135"},{"name":"B136"},{"name":"C137"},{"name":"D138"},{"name":"E139"},{"name":"F140"},{"name":"G141"}],"links":[{"source":0,"target":1,"value":375},{"source":0,"target":2,"value":27},{"source":0,"target":3,"value":15},{"source":0,"target":4,"value":8},{"source":0,"target":5,"value":6},{"source":0,"target":6,"value":4},{"source":0,"target":7,"value":3},{"source":0,"target":8,"value":3},{"source":0,"target":9,"value":2},{"source":0,"target":10,"value":2},{"source":0,"target":11,"value":2},{"source":0,"target":12,"value":2},{"source":0,"target":13,"value":2},{"source":0,"target":14,"value":2},{"source":0,"target":15,"value":1},{"source":0,"target":16,"value":1},{"source":0,"target":17,"value":1},{"source":0,"target":18,"value":1},{"source":0,"target":19,"value":1},{"source":0,"target":20,"value":1},{"source":0,"target":21,"value":1},{"source":0,"target":22,"value":1},{"source":0,"target":23,"value":87},{"source":0,"target":24,"value":24},{"source":0,"target":25,"value":20},{"source":0,"target":26,"value":20},{"source":0,"target":27,"value":19},{"source":0,"target":28,"value":17},{"source":0,"target":29,"value":12},{"source":0,"target":30,"value":6},{"source":0,"target":31,"value":5},{"source":0,"target":32,"value":5},{"source":0,"target":33,"value":4},{"source":0,"target":34,"value":4},{"source":0,"target":35,"value":3},{"source":0,"target":36,"value":3},{"source":0,"target":37,"value":3},{"source":0,"target":38,"value":3},{"source":0,"target":39,"value":3},{"source":0,"target":40,"value":3},{"source":0,"target":41,"value":2},{"source":0,"target":42,"value":2},{"source":0,"target":43,"value":2},{"source":0,"target":44,"value":2},{"source":0,"target":45,"value":1},{"source":0,"target":46,"value":1},{"source":0,"target":47,"value":1},{"source":0,"target":48,"value":1},{"source":0,"target":49,"value":1},{"source":0,"target":50,"value":1},{"source":0,"target":51,"value":1},{"source":0,"target":52,"value":1},{"source":0,"target":53,"value":1},{"source":0,"target":54,"value":1},{"source":0,"target":55,"value":34},{"source":0,"target":56,"value":13},{"source":0,"target":57,"value":8},{"source":0,"target":58,"value":8},{"source":0,"target":59,"value":5},{"source":0,"target":60,"value":5},{"source":0,"target":61,"value":4},{"source":0,"target":62,"value":4},{"source":0,"target":63,"value":3},{"source":0,"target":64,"value":3},{"source":0,"target":65,"value":3},{"source":0,"target":66,"value":2},{"source":0,"target":67,"value":2},{"source":0,"target":68,"value":2},{"source":0,"target":69,"value":2},{"source":0,"target":70,"value":2},{"source":0,"target":71,"value":2},{"source":0,"target":72,"value":2},{"source":0,"target":73,"value":1},{"source":0,"target":74,"value":1},{"source":0,"target":75,"value":1},{"source":0,"target":76,"value":1},{"source":0,"target":77,"value":1},{"source":0,"target":78,"value":1},{"source":0,"target":79,"value":1},{"source":0,"target":80,"value":1},{"source":0,"target":81,"value":1},{"source":0,"target":82,"value":1},{"source":0,"target":83,"value":1},{"source":0,"target":84,"value":1},{"source":0,"target":85,"value":1},{"source":0,"target":86,"value":1},{"source":0,"target":87,"value":1},{"source":0,"target":88,"value":1},{"source":0,"target":89,"value":1},{"source":0,"target":90,"value":1},{"source":0,"target":91,"value":1},{"source":0,"target":92,"value":1},{"source":0,"target":93,"value":1},{"source":0,"target":94,"value":1},{"source":0,"target":95,"value":11},{"source":0,"target":96,"value":7},{"source":0,"target":97,"value":6},{"source":0,"target":98,"value":3},{"source":0,"target":99,"value":3},{"source":0,"target":100,"value":2},{"source":0,"target":101,"value":1},{"source":0,"target":102,"value":1},{"source":0,"target":103,"value":1},{"source":0,"target":104,"value":1},{"source":0,"target":105,"value":1},{"source":0,"target":106,"value":1},{"source":0,"target":107,"value":1},{"source":0,"target":108,"value":1},{"source":0,"target":109,"value":1},{"source":0,"target":110,"value":1},{"source":0,"target":111,"value":1},{"source":0,"target":112,"value":1},{"source":0,"target":113,"value":1},{"source":0,"target":114,"value":1},{"source":0,"target":115,"value":1},{"source":0,"target":116,"value":1},{"source":0,"target":117,"value":9},{"source":0,"target":118,"value":7},{"source":0,"target":119,"value":1},{"source":0,"target":120,"value":1},{"source":0,"target":121,"value":1},{"source":0,"target":122,"value":1},{"source":0,"target":123,"value":8},{"source":0,"target":124,"value":4},{"source":0,"target":125,"value":2},{"source":0,"target":126,"value":2},{"source":0,"target":127,"value":2},{"source":0,"target":128,"value":1},{"source":0,"target":129,"value":4},{"source":0,"target":130,"value":4},{"source":0,"target":131,"value":3},{"source":0,"target":132,"value":2},{"source":0,"target":133,"value":2},{"source":0,"target":134,"value":1},{"source":0,"target":135,"value":1},{"source":0,"target":136,"value":1},{"source":0,"target":137,"value":8},{"source":0,"target":138,"value":3},{"source":0,"target":139,"value":2},{"source":0,"target":140,"value":1},{"source":0,"target":141,"value":1},{"source":0,"target":142,"value":4},{"source":0,"target":143,"value":3},{"source":0,"target":144,"value":2},{"source":0,"target":145,"value":2},{"source":0,"target":146,"value":2},{"source":0,"target":147,"value":1},{"source":0,"target":148,"value":6},{"source":0,"target":149,"value":3},{"source":0,"target":150,"value":1},{"source":0,"target":151,"value":1},{"source":0,"target":152,"value":1},{"source":0,"target":153,"value":1},{"source":0,"target":154,"value":1},{"source":0,"target":155,"value":6},{"source":0,"target":156,"value":2},{"source":0,"target":157,"value":2},{"source":0,"target":158,"value":1},{"source":0,"target":159,"value":1},{"source":0,"target":160,"value":1},{"source":0,"target":161,"value":2},{"source":0,"target":162,"value":1},{"source":0,"target":163,"value":1},{"source":0,"target":164,"value":1},{"source":0,"target":165,"value":1},{"source":0,"target":166,"value":1},{"source":0,"target":167,"value":1},{"source":0,"target":168,"value":1},{"source":0,"target":169,"value":5},{"source":0,"target":170,"value":2},{"source":0,"target":171,"value":1},{"source":0,"target":172,"value":2},{"source":0,"target":173,"value":2},{"source":0,"target":174,"value":1},{"source":0,"target":175,"value":1},{"source":0,"target":176,"value":1},{"source":0,"target":177,"value":1},{"source":0,"target":178,"value":2},{"source":0,"target":179,"value":2},{"source":0,"target":180,"value":1},{"source":0,"target":181,"value":1},{"source":0,"target":182,"value":1},{"source":0,"target":183,"value":1},{"source":0,"target":184,"value":1},{"source":0,"target":185,"value":1},{"source":0,"target":186,"value":1},{"source":0,"target":187,"value":1},{"source":0,"target":188,"value":1},{"source":0,"target":189,"value":1},{"source":0,"target":190,"value":2},{"source":0,"target":191,"value":1},{"source":0,"target":192,"value":1},{"source":0,"target":194},{"source":194,"target":193},{"source":0,"target":195},{"source":195,"target":193},{"source":0,"target":196},{"source":196,"target":193},{"source":0,"target":197,"value":27},{"source":0,"target":199},{"source":199,"target":198},{"source":0,"target":201},{"source":201,"target":200},{"source":0,"target":194},{"source":194,"target":202},{"source":0,"target":195},{"source":195,"target":202},{"source":0,"target":196},{"source":196,"target":202},{"source":0,"target":204},{"source":204,"target":203},{"source":0,"target":206},{"source":206,"target":205},{"source":0,"target":208},{"source":208,"target":207},{"source":0,"target":201},{"source":201,"target":209},{"source":0,"target":194},{"source":194,"target":210},{"source":0,"target":199},{"source":199,"target":211},{"source":0,"target":213},{"source":213,"target":212},{"source":0,"target":214,"value":2},{"source":0,"target":216},{"source":216,"target":215},{"source":0,"target":218},{"source":218,"target":217},{"source":0,"target":218},{"source":218,"target":219},{"source":0,"target":204},{"source":204,"target":220},{"source":0,"target":194},{"source":194,"target":221},{"source":0,"target":194},{"source":194,"target":222},{"source":0,"target":208},{"source":208,"target":223},{"source":0,"target":225},{"source":225,"target":224},{"source":0,"target":227},{"source":227,"target":226},{"source":0,"target":229},{"source":229,"target":228},{"source":0,"target":230,"value":1},{"source":0,"target":231,"value":1},{"source":0,"target":232,"value":1},{"source":0,"target":233,"value":1},{"source":0,"target":234,"value":1},{"source":0,"target":235,"value":1},{"source":0,"target":236,"value":1},{"source":0,"target":237,"value":1},{"source":0,"target":194},{"source":194,"target":238},{"source":0,"target":194},{"source":194,"target":239},{"source":0,"target":194},{"source":194,"target":240}]}; zoom = d3.zoom() .scaleExtent([0.1, 10]) .on("zoom", zoomed); var container = d3.select("#network_group"); var svgForce = container .append("svg") .attr("id", "network_svg") .attr("width", width_network) .attr("height", height_network); gMain = svgForce.append("g") .attr("class","gMain"); var rect = gMain.append('rect') .attr('width', width_network) .attr('height', height_network) .style('fill', 'white'); gForce = gMain.append("g"); gMain.call(zoom) .on("dblclick.zoom", null); var simulation = d3.forceSimulation(network.nodes).force("charge", d3.forceManyBody().strength(-200)) .force("collision", d3.forceCollide().radius(CIRCLE_RADIUS*2)) .force("x", d3.forceX(width_network/2).strength(0.015)) .force("y", d3.forceY(height_network/2).strength(0.02)) .force("center", d3.forceCenter(width_network/2, height_network/2)) .force("link", d3.forceLink(network.links).distance(70)); var gBrushHolder = gForce.append('g'); var gBrush = null; link = gForce.append("g") .attr("class", "force_links") .selectAll("line") .data(network.links) .enter() .append("line") .attr("stroke-width", 2); // define nodes node = gForce.append("g") .attr("class", "force_nodes") .selectAll("circle") .data(network.nodes) .enter() .append("circle") .attr("r", CIRCLE_RADIUS) .on("mouseover", function (d) { d3.select(this).append("title") .text(function (d) { return d.name; }); }) .on("mouseout", function (d) { d3.select(this).select("title").remove(); }) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended)); // define node labels nodeLabel = gForce.append("g") .attr("class", "label_nodes") .selectAll("text") .data(network.nodes) .enter() .append("text") .text(function (d) { return d.name; }) .style("text-anchor", "start"); simulation.on("tick", ticked); function ticked() { node.attr("cx", function (d) { return d.x; }) .attr("cy", function (d) { return d.y; }); 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; }); nodeLabel.attr("x", function (d) { return d.x; }) .attr("y", function (d) { return d.y - 13; }); } var brushMode = false; var brushing = false; var brush = d3.brush() .extent([[0,0],[width_network,height_network]]) .on("start", brush_start) .on("brush", brushed) .on("end", brush_end); 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); if (d.fixed) { d.fixed = false; d.fx = null; d.fy = null; d3.select(this) .style("fill","lightsteelblue") .style("stroke", "#fff") .style("stroke-width", "1.5px"); } else { d3.select(this) .style("stroke", "lightsteelblue") .style("fill", "white") .style("stroke-width", 9); d.fixed = true; } } function zoomed() { var transform = d3.event.transform; gForce.attr("transform", transform); } if (network.nodes.length > 80) { zoom.translateTo(svgForce, (width_network - width_network * 0.4) / 2, (height_network - height_network * 0.4) / 2); zoom.scaleTo(svgForce, 0.4); } function brush_start(){ brushing = true; node.each(function(d) { d.previouslySelected = ctrlKey && d.selected; }); } rect.on('click', () => { node.each(function(d) { d.selected = false; d.previouslySelected = false; }); node.classed("selected", false); }); function brushed() { if (!d3.event.sourceEvent) return; if (!d3.event.selection) return; var extent = d3.event.selection; node.classed("selected", function(d) { return d.selected = d.previouslySelected ^ (extent[0][0] <= d.x && d.x < extent[1][0] && extent[0][1] <= d.y && d.y < extent[1][1]); }); } function brush_end(){ if (!d3.event.sourceEvent) return; if (!d3.event.selection) return; if (!gBrush) return; gBrush.call(brush.move, null); if (!brushMode) { // the shift key has been release before we ended our brushing gBrush.remove(); gBrush = null; } brushing = false; } d3.select('body').on('keydown', keydown); d3.select('body').on('keyup', keyup); var ctrlKey; function keydown() { ctrlKey = d3.event.ctrlKey; if (ctrlKey) { if (gBrush) return; brushMode = true; if (!gBrush) { gBrush = gBrushHolder.append("g").attr("class", "brush"); gBrush.call(brush); } } } function keyup() { ctrlKey = false; brushMode = false; if (!gBrush) return; if (!brushing) { gBrush.remove(); gBrush = null; } } .force_nodes { fill: lightsteelblue; } .force_links { stroke: gray; } .force_nodes .selected { stroke: red; } .label_nodes text { cursor: pointer; } <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script> <div id="network_group"></div> You can find a working version of the code also in this JSFiddle.
The problem with your code right now is that you're appending the brush group to a group that you later translate in the zoom function: var gBrushHolder = gForce.append('g'); Instead of that, append the brush group to the main group: var gBrushHolder = gMain.append('g'); Then, use a variable to track the zoom's translate and scale... //declare it: let currentZoom; //update its value in the zoom function: currentZoom = d3.event.transform; Finally, use that value to get the SVG position of the nodes: node.classed("selected", function(d) { return d.selected = d.previouslySelected ^ (extent[0][0] <= (d.x * currentZoom.k + currentZoom.x) && (d.x * currentZoom.k + currentZoom.x) < extent[1][0] && extent[0][1] <= (d.y * currentZoom.k + currentZoom.y) && (d.y * currentZoom.k + currentZoom.y) < extent[1][1]); }); Here is the updated JSFiddle: https://jsfiddle.net/uxqf7tp2/
how to adjust size of force directed graph in d3.js?
I have implemented a force directed graph which visualizes shared borders between countries.The layout of graph goes out of svg boundaries,is there way to resize the graph size,so that graph layout stays within graph boundaries.can size of simulation be pre set to adjust to the change in the width and height of window? link to codepen let request = new XMLHttpRequest(); request.addEventListener("load", loaded); function loaded() { const data = JSON.parse(request.responseText); var nodes = data.nodes; var links = data.links; // sets up svg var svg = d3.select("svg"), width = +svg.attr("width"), height = +svg.attr("height"); // handle color of the nodes i gueess,gotta know what schemeCategory method does var color = d3.scaleOrdinal(d3.schemeCategory20); // starts simulation var simulation = d3 .forceSimulation() .force("link", d3.forceLink()) .force("charge", d3.forceManyBody()) .force("center", d3.forceCenter(width /2, height / 2)) // creates lines in graph, var link = svg .append("g") .attr("class", "links") .selectAll("line") .data(links) .enter() .append("line") .attr("stroke-width", function(d) { return Math.sqrt(3); }); //creates nodes..for this example,you need to set node to images var node = svg .append("g") .attr("class", "nodes") .selectAll(".node") //pass node data .data(nodes) .enter() .append("image") .attr("xlink:href",function(d){return "https://cdn.rawgit.com/hjnilsson/country-flags/master/svg/"+d.code+".svg" }) .attr("x", -8) .attr("y", -8) .attr("width", 16) .attr("height", 16) .call( d3 .drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended) ); node.append("title") .text(function(d) { return d.country; }); 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 + ")"; }); } 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; } } request.open( "GET", "https://www.cs.mun.ca/~h65ped/Public/country%20data%20for%20force%20directed%20graph/countries.json", true ); request.send(null);
The attractive forces of the built-in d3.forceManyBody can be modified with the strength() method so try something like // starts simulation var simulation = d3 .forceSimulation() .force("link", d3.forceLink()) .force("charge", d3.forceManyBody().strength(-5)) .force("center", d3.forceCenter(width /2, height / 2)) If that constrains the other items too closely then you would have to implement your own force method which can constrain items based on the SVG size. See d3.force for a description of what is happening under-the-hood and to see how you could produce your own function.
Force-Dragging-Graph D3.js error
I was able to replicate one of Mike Bostock's Force Dragging example from his blog However I am not sure why when I try to simulate it in my own style, it crashes. Here's a working code, mimicking exactly's Bostock's code, except using a different JSON data Here's the link to JSON data Codepen - Working Here's my own attempt : Codepen - Not Working $( document ).ready(function(){ const w = 1000; const h = 700; const margin = { top: 90, bottom: 90, left: 90, right: 90 } function title(){ } function render(data){ const width = w - (margin.left + margin.right); const height = h - (margin.up + margin.down); let svg = d3.select("#canvas") .append("svg") .attr("id","chart") .attr("width", w) .attr("height", h) let chart = svg.append("g") .classed("display", true) .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); let simulation = d3.forceSimulation() .force("link", d3.forceLink().id(function(d,i) { return i; })) .force("charge", d3.forceManyBody()) .force("center", d3.forceCenter(width / 2, height / 2)) let link = chart.append("g") .classed("links",true) .selectAll("line") .data(data.links) .enter() .append("line") let node = chart.append("g") .classed("nodes",true) .selectAll("circle") .data(data.nodes) .enter() .append("circle") .attr("r", 2.5) .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended) ); node.append("title") .text(function(d) { return d.country; }); simulation .nodes(data.nodes) .on("tick", ticked); simulation.force("link") .links(data.links); //functions provided by D3.js // 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; }); } 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; } } const url = 'https://raw.githubusercontent.com/DealPete/forceDirected/master/countries.json'; $.ajax({ type: "GET", dataType: "json", url: url, beforeSend: ()=>{ }, complete: () =>{ }, success: data =>{ render(data) }, fail: () =>{ console.log('failure!') }, error: () =>{ } }); });
Change the variable names up and down used for height calculation to top and bottom. Height attribute was set NaN due to this typo in margin variables. Change this line- const height = h - (margin.up + margin.down); to const height = h - (margin.top + margin.bottom); Updated codepen: http://codepen.io/anon/pen/XjVyGx
d3 v4 update/merge grouped data
I am trying to update data for a force simulation in D3 v4, similar to this unsuccessful attempt and semi-similar to this successful one. CodePen here Instead of joining the new nodes it appears to be doubling all the nodes (see pictures). It does not seem to be refreshing the graph correctly either. (initial graph) (after adding data) HTML: <button onclick="addData()">Add Data</button> <svg width="960" height="600"></svg> JavaScript: function addData() { graph.nodes.push({"id": "Extra1", "group": 11},{"id": "Extra2", "group": 11}) graph.links.push({"source": "Extra1", "target": "Valjean", "strength": 1},{"source": "Extra1", "target": "Extra2", "strength": 2}) update() simulation.restart() } var svg = d3.select("svg"), width = +svg.attr("width"), height = +svg.attr("height"); var link, linkEnter, nodeWrapper, nodeWrapperEnter; var simulation = d3.forceSimulation() .force("link", d3.forceLink().id(function(d) { return d.id; })) .force("charge", d3.forceManyBody()) .force("center", d3.forceCenter(width / 2, height / 2)) .on("tick", ticked); var allLinkG = svg.append("g") .attr("class", "allLinkG") var allNodeG = svg.append("g") .attr("class", "allNodeG") update() simulation.restart() function update(){ link = allLinkG .selectAll("line") .data(graph.links, function(d){ return d.id }) link.exit().remove() linkEnter = link .enter().append("line"); link = linkEnter.merge(link).attr("class","merged"); nodeWrapper = allNodeG .selectAll("nodeWrapper") .data(graph.nodes, function(d) { return d.id; }) nodeWrapper.exit().remove(); nodeWrapperEnter = nodeWrapper.enter() .append("g").attr("class","nodeWrapper") .append("circle") .attr("r", 2.5) nodeWrapper = nodeWrapperEnter .merge(nodeWrapper).attr("class","merged"); simulation .nodes(graph.nodes); simulation.force("link") .links(graph.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; }); nodeWrapper .attr("cx", function(d) { return d.x; }) .attr("cy", function(d) { return d.y; }); } Thanks very much for any help.
A few issues here: Your selectAll is selecting by element type nodeWrapper, you meant by class .nodeWrapper. You change the class name to "merged" after your .merge, you can't do this as it breaks future selections by .nodeWrapper class. When you .merge your selection, you are merging circles with gs. You should stay consistent and operate on the gs only. Quick refactor: function update() { link = allLinkG .selectAll("line") .data(graph.links, function(d) { return d.id }) link.exit().remove() linkEnter = link .enter().append("line"); link = linkEnter.merge(link).attr("class", "merged"); nodeWrapper = allNodeG .selectAll(".nodeWrapper") //<-- class nodeWrapper .data(graph.nodes, function(d) { return d.id; }) nodeWrapperEnter = nodeWrapper.enter() .append("g").attr("class", "nodeWrapper"); //<-- enter selection should be gs nodeWrapperEnter //<-- append your circles .append("circle") .attr("r", 2.5) nodeWrapper = nodeWrapperEnter //<-- merge, don't change class .merge(nodeWrapper); nodeWrapper.exit().remove(); //<-- and the exit simulation .nodes(graph.nodes); simulation.force("link") .links(graph.links); } Note, I also modified your tick function to operate on the g instead of the circle: nodeWrapper .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); Full code is here.