Basics of D3's Force-directed Layout - javascript

I'm plowing into the exciting world of force-directed layouts with d3.js. I've got a grasp of the fundamentals of d3, but I can't figure out the basic system for setting up a force-directed layout.
Right now, I'm trying to create a simple layout with a few unconnected bubbles that float to the center. Pretty simple right!? The circles with the correct are created, but nothing happens.
Edit: the problem seems to be that the force.nodes() returns the initial data array. On working scripts, force.nodes() returns an array of objects.
Here's my code:
<script>
$(function(){
var width = 600,
height = 400;
var data = [2,5,7,3,4,6,3,6];
//create chart
var chart = d3.select('body').append('svg')
.attr('class','chart')
.attr('width', width)
.attr('height', height);
//create force layout
var force = d3.layout.force()
.gravity(30)
.alpha(.2)
.size([width, height])
.nodes(data)
.links([])
.charge(30)
.start();
//create visuals
var circles = chart.selectAll('.circle')
.data(force.nodes()) //what should I put here???
.enter()
.append('circle')
.attr('class','circles')
.attr('r', function(d) { return d; });
//update locations
force.on("tick", function(e) {
circles.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
</script>

Here was the problem. The array you feed into force.nodes(array) needs to be an array of objects.
So:
var data = [{number:1, value:20}, {number:2, value:30}...];
works. Or even just
var data=[{value:1},{value:2}];
makes the script work fine.

You need to update cx and cy in force.on handler.
//update locations
force.on("tick", function(e) {
circles.attr("cx", function(d) { return d.x - deltaX(d, e) ; })
.attr("cy", function(d) { return d.y - deltaY(d, e); });
});
And functions deltaX and deltaY depends on your force model.

Related

Clustering bubbles in d3.js

I've been trying to do a widget with some information from Google Analytics. I am getting the real time visitors and then displaying them in bubbles with flags with the help of d3.js. I do manage to get them on the screen and the force simulation is working properly. However I can't seem to grasp how could I cluster the bubbles according to their respective country, as I don't know which countries will be displayed in advance, and I'm not good at web programming so as do some on the fly links and use the forceLink. Maybe there is a better way that I'm missing? I would greatly appreciate any pointers that you could give me.
(function() {
var width = 500;
height = 500;
var svg = d3.select("#chart")
.append("svg")
.attr("height", height)
.attr("width", width)
.append("g")
.attr("transform", "translate(0,0)")
// SO question preparation :
var data_csv = "Country,Count\nUkraine,1\nDenmark,1\nDenmark,1";
data = d3.csvParse(data_csv);
d3.queue().await(ready, data);
var forceX = d3.forceX(function(d) {
return width / 2
}).strength(0.05)
var forceY = d3.forceY(function(d) {
return height / 2
}).strength(0.05)
var simulation = d3.forceSimulation()
.force("xTowardsTheCenter", forceX)
.force("yTowardsTheCenter", forceY)
.force("antiColliding", d3.forceCollide(function(d) {
return radiusScale(d.Count) + 1;
}))
.force("charge", d3.forceManyBody().strength(-15))
// this function sets up a variation scale so that the circles can have different sizes without
// occupying all of the screen
var radiusScale = d3.scaleSqrt().domain([1, 40]).range([30, 130]);
function ready(error, datapoints) {
var circles = svg.selectAll(".Country")
.data(datapoints)
.enter().append("circle")
.attr("id", function(d) {
return d.Country;
})
.attr("class", "country")
.attr("r", function(d) {
return radiusScale(d.Count);
})
.attr("text", function(d) {
return d.Count;
})
.style("stroke", "black")
.style("fill", "blue")
simulation.nodes(datapoints)
.on('tick', ticked)
function ticked() {
circles
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
}
})();
<!DOCTYPE html>
<html>
<title>Neets visitors </title>
<b>Neets visitors</b>
<body>
<div id="chart"></div>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="bubble.js"></script>
</body>
</html>
Expected result

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: Attributes after enter().append() not setting

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.

d3.js force directed graph sphere

I have adapted Christopher Manning's force-directed graph on a sphere. I would like to have the graph settle down and then rotate the sphere without changing the relationships among the points in the graph. Instead, when I drag, it seems to drag the graph, and not rotate the sphere. Dragging the graph activates the force start. If I turn off force.start() nothing changes.
var svg = d3.select("#cluster").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.drag()
.origin(function() { var r = projection.rotate(); return {x: 2 * r[0], y: -2 * r[1]}; })
.on("drag", function() { force.start(); var r = [d3.event.x / 2, -d3.event.y / 2, projection.rotate()[2]]; t0 = Date.now(); origin = r; projection.rotate(r); }))
From http://bl.ocks.org/mbostock/3795040, I found that I could rotate the graticule, but then all of my links and nodes disappear.
var path = d3.geo.path()
.projection(projection);
var λ = d3.scale.linear()
.domain([0, width])
.range([-180, 180]);
var φ = d3.scale.linear()
.domain([0, height])
.range([90, -90]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.on("mousemove", function() {
var p = d3.mouse(this);
projection.rotate([λ(p[0]), φ(p[1])]);
svg.selectAll("path").attr("d", path);
});
Links and nodes get added like this:
var link = svg.selectAll("path.link")
.data(graph.links)
.enter().append("path").attr("class", "link")
.attr ("stroke-width", function(d){return d.value/3});
var node = svg.selectAll("path.node")
.data(graph.nodes)
.enter()
.append("g")
.attr("class", "gnode")
.attr("text-anchor", "middle")
.append("path").attr("class", "node")
.style("fill", function(d) { return d.color; })
.style("stroke", function(d) { return d3.rgb(fill(d.group)).darker(); })
What I would like to accomplish: when the graph settles into position, I would like to use the drag gesture to rotate the sphere with the nodes and links on it.
How do I make this happen?
This could be done by adjusting the handler registered for the drag event on the svg which is defined in your first snippet. This requires two edits:
Get rid of force.start() because you don't want to restart the force on drag.
Trigger the rendering of nodes and links after the projection has been updated, which can easily be done by calling tick(). If the force had not been deactivated by step 1., this would be called repeatedly because the function is registered as the handler for the force layout's tick event. Now, that you have deactivated the force, you will have to call it explicitely.
The reformatted code will look like:
.on("drag", function() {
//force.start(); // 1. Don't restart the force.
var r = [d3.event.x / 2, -d3.event.y / 2, projection.rotate()[2]];
t0 = Date.now();
origin = r;
projection.rotate(r);
tick(); // 2. Trigger the rendering after adjusting the projection.
}))
Have a look at this working example.
This may rotate along one axis
var rotateScale = d3.scale.linear().domain([0, width]).range([-180,180]);//width of SVG
d3.select("svg").on("mousedown",startRotating).on("mouseup",stopRotating);
function startRotating() {
d3.select("svg").on("mousemove",function() {
var p = d3.mouse(this);
projection.rotate([rotateScale(p[0]),0]);
});
}
function stopRotating() {
d3.select("svg").on("mousemove",null);
}

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

Categories