D3.js: Attributes after enter().append() not setting - javascript

Like most people, I'm struggling with D3's data join mechanics. I have read every article on the subject, good and (mostly) bad. Christian Behrens' guest-and-chair party analogy is probably the best, though I warn readers that he neglects us about 2/3 through, beginning with "Now our update() function performs two different sets of actions" -- he doesn't clarify here that, apparently, DOM construction (append/remove) calls are specifically ignored by the update selection, while attribute calls are processed by all three selection types, despite all appearing seamlessly in one method chain. (For his part, Mike Bostock's several efforts at explaining data joins and method chaining range from mildly condescending to entirely complicating matters.)
I still have a problem understanding the membrane between data() and enter(), specifically when to save a variable and how calls in a given chain operate on what objects, and also which chain return value my variable saves, and how I should know that (clearly, attr does not affect the variable, but in a chain that includes a series of selects, data, and enters, which is returned?); hence my mild criticism of the otherwise excellent Behrens essay, because it has so much promise there.
Below, I have a force layout (could be any layout) that displays two nodes on startup, and if you click any of the nodes, a third should be added.
var graph = {
"nodes":[ {"name":"1" }, {"name":"2" } ],
"links":[ {"source":0,"target":1} ]
}
var width = 500, height = 400;
var force = d3.layout.force()
.size([width, height]);
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
var rect = svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all");
var container = svg.append("g");
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = container.append("g")
.attr("class", "links")
.selectAll(".link")
.data(graph.links)
.enter()
.append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var nodes = container.append("g")
.attr("class", "nodes");
function update() {
var n = nodes.selectAll(".node").data(graph.nodes);
ne = n.enter()
.append("g")
.attr("class", "node")
.attr("cx", function(d) { return d.x; }) // sets on initial enter() but not on click
.attr("cy", function(d) { return d.y; }); // sets on initial enter() but not on click
ne.append("rect")
.attr("width", "20")
.attr("height", "20")
.attr("fill", "red");
return n;
} // end update()
var node = update();
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 + ")"; });
});
node.on("click", function(d) {
graph.nodes.push({ "name":"3" });
update();
}); // end .on("click")
I made an update() function to economize, following Behrens. Setting cx and cy works fine initially but not when you click on a node. New nodes stay at [0,0]. I think there must be a problem in the way I manage the arguments and returns on update(). A better way to do this, one that actually works?

You're just missing two small things. First, you need to restart the force layout so that the node positions are updated, and you need to update node which you're using inside the tick handler function:
node.on("click", function(d) {
graph.nodes.push({ "name":"3" });
node = update();
force.start();
});
Complete demo here.

I ended up solving this on my own with some experimentation; it seems to have had to do with the fact that I needed to put the force.on("tick"...) and node.on("click") functions inside the update() function. If I didn't have the tick processor in there, it would not create a transform for the new node, which appears to conform to what I was seeing. In addition, I must call force.start() again after updating. I will post the working code at my next opportunity: it is on an offline laptop right now.

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>

My recursive function is too fast, to animate paths

I'm visualising voyages on a map with D3.js' path. My data is in a JSON file like the following format:
[{"begin_date":1519,"trips":[[-13.821772,14.294839],[-9.517688,-7.958521],[0.598434,-34.704567],[18.374673,-86.850335]]},
{"begin_date":1549,"trips":[[12.821772,-16.294839],[5.517688,-20.958521],[13.598434,-54.704567],[18.374673,-86.850335]]},
{"begin_date":1549,"trips":[[12.821772,-16.294839],[5.517688,-20.958521],[13.598434,-54.704567],[18.374673,-86.850335]]}]
As can be seen, sometimes there are multiple voyages for a year. The following recursive function works:
d3.json("data/output.json", function(error, trips) {
if (error) throw error
var nest = d3.nest()
.key(function(d){
return d.begin_date;
})
.entries(trips)
var trip = svg.selectAll(".voyage");
// Add the year label; the value is set on transition.
var label = svg.append("text")
.attr("class", "year label")
.attr("text-anchor", "end")
.attr("y", height - 400)
.attr("x", width+150)
.text(function(d) {return d});
var pnt = 0;
doTransition();
function doTransition() {
trip.data(nest[pnt].values).enter()
.append("path")
.attr("class", "voyage")
.style("stroke", colorTrips)
.attr('d', function(d) {label.text(d.begin_date); return lineGen(d.trips.map(reversed).map(projection))})
.call(function transition(path) {
path.transition()
.duration(500)
.attrTween("stroke-dasharray", tweenDash)
.each("end", function(d) {
d3.selectAll(".voyage")
.remove()
pnt++;
if (pnt >= nest.length){return;}
doTransition();
})
})
}
Some voyages plot as they should
However some of them, are never plotted (I can see in the log) and it jumps from year 1545-1569 despite there being data points in between. I suspect it is due to the recursive function calling itself before the transition is finished. But I am also not sure, in the slightest.
Hope it is sufficient, I am new to D3.js, and suddenly found myself out of depth.

D3 chart can't update -- enter and exit property of selection both empty

I'm trying to make a scatter plot using a .json file. It will let the user to select which group of data in the json file to be displayed. So I'm trying to use the update pattern.
The following code will make the first drawing, but every time selectGroup() is called(the code is in the html file), nothing got updated. The console.log(selection) did come back with a new array each time, but the enter and exit property of that selection is always empty.
Can anyone help me take a look? Thanks a lot!
var margin = {
top: 30,
right: 40,
bottom: 30,
left: 40
}
var width = 640 - margin.right - margin.left,
height = 360 - margin.top - margin.bottom;
var dataGroup;
var groupNumDefault = "I";
var maxX, maxY;
var svg, xAxis, xScale, yAxis, yScale;
//select and read data by group
function init() {
d3.json("data.json", function (d) {
maxX = d3.max(d, function (d) {
return d.x;
});
maxY = d3.max(d, function (d) {
return d.y;
});
console.log(maxY);
svg = d3.select("svg")
.attr("id", "scatter_plot")
.attr("width", 960)
.attr("height", 500)
.append("g")
.attr("id", "drawing_area")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//x-axis
xScale = d3.scale.linear().range([0, width]).domain([0, maxX]);
xAxis = d3.svg.axis()
.scale(xScale).orient("bottom").ticks(6);
//y-axis
yScale = d3.scale.linear().range([0, height]).domain([maxY, 0]);
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(6);
svg.append("g")
.attr("class", "x_axis")
.attr("transform", "translate(0," + (height) + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y_axis")
.call(yAxis);
});
selectGroup(groupNumDefault);
}
//update data
function selectGroup(groupNum) {
d3.json("/data.json", function (d) {
dataGroup = d.filter(function (el) {
return el.group == groupNum;
});
console.log(dataGroup);
drawChart(dataGroup);
});
}
//drawing function
function drawChart(data) {
var selection = d3.select("svg").selectAll("circle")
.data(data);
console.log(selection);
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
selection.exit().remove();
}
init();
The problem here is on two fronts:
Firstly, your lack of a key function in your data() call means data is matched by index (position in data array) by default, which will mean no enter and exit selections if the old and current datasets sent to data() are of the same size. Instead, most (perhaps all) of the data will be put in the update selection when d3 matches by index (first datum in old dataset = first datum in new dataset, second datum in old dataset = second datum in new dataset etc etc)
var selection = d3.select("svg").selectAll("circle")
.data(data);
See: https://bl.ocks.org/mbostock/3808221
Basically, you need your data call adjusted to something like this (if your data has an .id property or anything else that can uniquely identify each datum)
var selection = d3.select("svg").selectAll("circle")
.data(data, function(d) { return d.id; });
This will generate enter() and exit() (and update) selections based on the data's actual contents rather than just their index.
Secondly, not everything the second time round is guaranteed be in the enter or exit selections. Some data may be just an update of existing data and not in either of those selections (in your case it may be intended to be completely new each time). However, given the situation just described above it's pretty much guaranteed most of your data will be in the update selection, some of it by mistake. To show updates you will need to alter the code like this (I'm assuming d3 v3 here, apparently it's slightly different for v4)
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
// this new bit is the update selection (which includes the just added enter selection
// now, the syntax is different in v4)
selection // v3 version
// .merge(selection) // v4 version (remove semi-colon off preceding enter statement)
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
selection.exit().remove();
Those two changes should see your visualisation working, unless of course the problem is something as simple as an empty set of data the second time around which would also explain things :-)

d3.js: Confusion about the order in which the code is executed

I am trying to make an interactive bar chart in D3.js
I uploaded everything to github for easy reference. I also included index.html at the end of my question.
My starting point is data.json containing an array of 7 items (i.e. countries). Each country has an attribute 'name' and four other attributes. These represent the exposition of private banks and the state to Greek debt for the years 2009 and 2014.
My goal is to create a bar chart that starts by showing the exposition of each country's banks and public sector in 2009 (so two bars for each country) and that changes to the year 2014 once the user clicks on the appropriate button.
I had managed to make it all work nicely! However, I had to create manually separate lists for each (sub-)dataset I needed to use. For example I created one called y2009 which included the exposition of bank and state for country 1, then the same for country 2, an so on..
(I left one of the list and commented it out on line 43)
I wanted to make my code more flexible so I created a for loop that extracts the data and creates the lists for me. (see lines 46-60). This did not work because the for loops would start before the data was actually loaded. Hence I would end up with empty lists.
So I grouped the for loops into a function (prepare()) and executed that function within the function that loads the data (lines 18-30). This fixed that issue...
..and created a new one! The two functions that should set the scales (see lines 67-73) do not work because their calculations require on one of the lists created by the for loops (namely 'total').
(I assume this is due to the list being created after the scale methods are called.)
The curious thing is that if I run the script, then copy in the console the xScale and yScale functions, and then copy the draw function (lines 101-212) everything works.
Hence I tried to group everything into functions (e.g. setScales, draw) so that I would call them in the order I want at the end of the script (lines 214-215) but this creates problem because certain variables (e.g. xScale and yScale) need to be global.
I also tried to first create them in the global space and then modify them through setScales. This did not work either.
Summing up, wait I don't understand is:
In which order should I write the code to make things work(again)? Is it a good idea to wrap operations within functions (e.g. setting the scales, drawing bars and labels) and then calling the function in the right order?
Which type of object is created with the scale method? I am confused on whether they are actual functions.
I hope this was not too much of a pain to read and thank everyone who made it through!
Fede
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="d3.min.js"></script>
</head>
<body>
<p>Introductory text here!</p>
<p>
<button id="change2009"> 2009 </button>
<button id="change2014"> 2014 </button>
</p>
<div id="country"></div>
<script type="text/javascript">
d3.json("data.json", function(error, json) {
if (error) {
console.log(error);
} else{
console.log(json);
dataset=json;
}
prepare (dataset);
});
//load data
var dataset;
var bank09=[];
var state09=[];
var bank14=[];
var state14=[];
var y2009=[];
var y2014=[];
var total=[];
var xScale;
var yScale;
//var total = [4.76, 0, 0.12, 6.36, 4.21, 0, 0.04, 7.96, 78.82, 0, 1.81, 46.56, 45, 0, 13.51, 61.74, 6.86, 0, 1.06, 40.87, 12.21, 0, 1.22, 13.06, 1.21, 0, 0.39, 27.35];
function prepare (dataset){
for (i in dataset) {bank09.push(dataset[i].bank09);
state09.push(dataset[i].state09);
bank14.push(dataset[i].bank14);
state14.push(dataset[i].state14);
y2009.push(dataset[i].bank09);
y2009.push(dataset[i].state09);
y2014.push(dataset[i].bank14);
y2014.push(dataset[i].state14);
total.push(dataset[i].bank09);
total.push(dataset[i].state09);
total.push(dataset[i].bank14);
total.push(dataset[i].state14);
}
}
//overwrite dataset
dataset2=y2009;
//scales
function setScales () {
var xScale = d3.scale.ordinal()
.domain(d3.range(total.length/2))
.rangeRoundBands([0, w], 0.1);
var yScale = d3.scale.linear()
.domain([0, d3.max(total)])
.range([0, h]);
console.log(yScale(89));
}
//layout
var w = 600;
var h = 600;
var barPadding = 1;
//coountry names
var country = ["Austria", "Belgium", "France", "Germany", "Italy", "Holland", "Spain"];
d3.select("#country")
.data(country)
.enter()
.append("rect")
.attr("class", "country")
//.append("text")
//.text(function(d){
// return d;
// })
//draw svg
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
function draw () {
//draw bars
svg.selectAll("rect")
.data(dataset2)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d){
return h - yScale(d);
})
.attr("width", xScale.rangeBand)
.attr("height", function(d) {
return yScale(d);
})
.attr("fill", "black");
//add labels
svg.selectAll("text")
.data(dataset2)
.enter()
.append("text")
.text(function(d){
return d;
})
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", "12px")
.attr("fill", "red")
.attr("x", function(d, i){
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
if (d<3) {
return h - 15;
} else {
return h - yScale(d) + 15;}
})
//interactivity
d3.select("#change2014")
.on("click", function() {
//update data
dataset2=y2014;
//update bars
svg.selectAll("rect")
.data(dataset2)
.transition()
.duration(3000)
.attr("y", function(d){
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
//update labels
svg.selectAll("text")
.data(dataset2)
.transition()
.duration(3000)
.text(function(d){
return d;
})
.attr("x", function(d, i){
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
if (d<3) {
return h - 15;
} else {
return h - yScale(d) + 15;}
})
})
d3.select("#change2009")
.on("click", function() {
//update data
dataset2=y2009;
//update bars
svg.selectAll("rect")
.data(dataset2)
.transition()
.duration(3000)
.attr("y", function(d){
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
//update labels
svg.selectAll("text")
.data(dataset2)
.transition()
.duration(3000)
.text(function(d){
return d;
})
.attr("x", function(d, i){
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
if (d<3) {
return h - 15;
} else {
return h - yScale(d) + 15;}
})
})
}
setScales ();
draw();
</script>
In which order should I write the code to make things work(again)? Is
it a good idea to wrap operations within functions (e.g. setting the
scales, drawing bars and labels) and then calling the function in the
right order?
As Lars pointed out, you can put everything inside the d3.json callback. This is because you only want to start rendering with D3 once you have the data. The d3.json method is asynchronous, which means that after you call d3.json(), the code afterwards will execute first before the function inside the d3.json method has finished. Check out http://rowanmanning.com/posts/javascript-for-beginners-async/ for more on asynchronous behavior in Javascript.
Given that you only want to start rendering when the d3.json method has completed, you could also just organize the other parts of your code into smaller functions and call some sort of initializer function from within the d3.json success callback, sort of like what you are doing with the prepare function. This is a cleaner approach and starts taking you towards a model-view paradigm.
Which type of object is created with the scale method? I am confused
on whether they are actual functions.
The scale method does return a function, but with additional functions added to its prototype. Try printing out "someScale.prototype" to see all of the various methods you can use. I'd also highly recommend Scott Murray's tutorial on D3. Here is the chapter on scales: http://alignedleft.com/tutorials/d3/scales

d3.js force-directed graph maintain constant link distances

Does anybody have an idea of how to maintain constant link distances while at the same time repulsing nodes?
Here's an example of the problem (this is the standard FDG example, but with fewer nodes).
var graph = {
"nodes":[
{"name":"a","group":1},
{"name":"a","group":1},
{"name":"a","group":1},
{"name":"a","group":1},
{"name":"b","group":8}
],
"links":[
{"source":1,"target":0,"value":1},
{"source":2,"target":0,"value":1},
{"source":3,"target":0,"value":1},
{"source":4,"target":0,"value":1}
]
};
var width = 300,
height = 300;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var drawGraph = function(graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var gnodes = svg.selectAll('g.gnode')
.data(graph.nodes)
.enter()
.append('g')
.classed('gnode', true)
.call(force.drag);
var node = gnodes.append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); });
node.append("title")
.text(function(d) { return d.name; });
var labels = gnodes.append("text")
.text(function(d) { return d.name; })
.attr('text-anchor', 'middle')
.attr('font-size', 8.0)
.attr('font-weight', 'bold')
.attr('y', 2.5)
.attr('fill', d3.rgb(50,50,50))
.attr('class', 'node-label')
.append("svg: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; })
.each(function(d) { console.log(Math.sqrt((d.source.x - d.target.x) * (d.source.x - d.target.x) + (d.source.y - d.target.y) * (d.source.y - d.target.y))); });
gnodes.attr("transform", function(d) {
return 'translate(' + [d.x, d.y] + ')';
});
});
};
drawGraph(graph);
http://jsfiddle.net/pkerpedjiev/vs3foo80/1/
There's one central node and four attached nodes. The links should all have a length of 30, but because of the repulsion forces, they settle down to lengths of 35. Is there a way to counteract that and make the link lengths to converge to their desired values of 30 while maintaining the repulsion between non-connected nodes?
This would be akin to making the link force much stronger than the repulsion force. Increasing that, however, leads to very unstable behaviour.
Another way of putting this question is, is there a way to spread the nodes as far apart from each other while maintaining the desired link lengths?
Yes, use .chargeDistance(30). The .chargeDistance() setting determines the maximum distance when charge is applied, and is infinite by default. A setting of 30 set your charge to only apply to nodes that are within 30px, and should give you the behavior you want.
The drawback of this is that on a large graph, you will no longer see add-on effects that unfold the graph faster and the layout will have a more localized appearance. To achieve something like that, I would suggest experimenting with a dynamic chargeDistance tied to the alpha parameter of the force algorithm (the cooldown) so that it starts at infinite and then moves toward 30 (or whatever) as the graph cools down.

Categories