d3 Force: Making sense of data binding - javascript

I can recreate the following 1000 times and have enough of an understanding to do so. But I'm trying to get my head around a few specific bits that I just 'do', rather than understand:
var w = 900,
h = 500;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.attr("style", "border: 1px solid grey;")
.on("mousemove", fn)
var force = d3.layout.force()
.size([w, h])
.on("tick", tick)
.gravity(0)
.charge(0)
.start()
function fn() {
var m = d3.mouse(this);
var point = {x: m[0], y: m[1]};
d3.select("#output").text(force.nodes().length)
var node = svg
.append("circle")
.data([point])
.attr("cx", function(d) {return d.x})
.attr("cy", function(d) {return d.y})
.attr("r", 0.1)
.transition().ease(Math.sqrt)
.attr("r", 5)
.transition().delay(1000)
.each("end", function() {
force.nodes().shift()
})
.remove()
force.nodes().push(point)
force.start()
}
function tick() {
svg.selectAll("circle")
.attr("cx", function(d) {return d.x})
.attr("cy", function(d) {return d.y})
}
In particular it's the data binding part I'm not sure about.
In function fn() (on mousemove of svg space) we define a new point and we need to do two things with it; push it into force.nodes() so that the x and y coordinates of the point can be manipulated by forces configured in the force layout, and we need to use the coordinates of the point to create and manipulate the visualisation.
So we create the point first off. We then build a circle to represent this point. We push the point into force.nodes() and after a short delay, we remove both the visualisation and the point from the force.nodes() array.
The bit I don't understand is how the visualisation and the point in the array stay "connected"?
Conjecture: The data point is an object which the force layout is constantly updating the x and y properties of. There is a "link" to this object bound to the circle element. The object is therefore easily accessed and used by the circle object, but not without us controlling that process. The circle is defined as having a cx and cy at point of its creation, but we need to keep accessing the underlying data to update its cx and cy?
If that's the case, how is the object "shared" by both force.nodes() and the circle element?
Or am I miles off the mark?
Also I have read a lot of documentation on this but I feel this is something more intrinsic to javascript rather than d3 necessarily, so it's not elaborated on in any literature I've so far read.

The link between the data structures that the force layout updates and the visualization (i.e. the DOM elements) is the tick event handler function. The tick event is generated by the force layout to signify that the force simulation has progressed another step (i.e. tick) and its internal state has changed. This signals that the visualization needs to be updated.
There are two parts to making this link happen. First, the data operated on by the force layout (i.e. the links and nodes) needs to be bound to DOM elements. This is done using the usual .selectAll().data().enter().append() pattern, usually in the initialisation code, sometimes in the tick event handler function. This establishes the link between data and DOM elements.
The second part to this is the code that updates the DOM elements when the force layout changes their positions. This is what happens in the tick event handler function. If you're not adding or removing elements, there's usually no need to rebind data and often you won't see the .selectAll().data() pattern, but only the code that actually updates the positions based on the data already bound to the elements (in your case this works even though you're changing the elements because the data binding happens in the function that updates the data for the force layout as well).
As an experiment, take an arbitrary force layout example and delete the tick event handler function -- you'll see that nothing happens at all even though the force layout is running.

Related

D3.js with Observable, problem with event handler [duplicate]

The documentation for d3.drag states the DOM element target of the drag event will be available in this to the callback:
When a specified event is dispatched, each listener will be invoked with the same context and arguments as selection.on listeners: the current datum d and index i, with the this context as the current DOM element.
But my call back is an object instance and this points to that object. So I need another way of accessing the current DOM element that is normally passed in this. How can I do it?
Use the second and the third arguments together to get this when this is not available:
d3.drag().on(typename, function(d, i, n) {
//here, 'this' is simply n[i]
})
For a detailed explanation, have a look at the article below that I wrote to deal with this in arrow functions. The issue is different from yours, but the explanation is the same.
Here is a basic demo, try to drag a circle and look at the console:
var data = d3.range(5)
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 100);
var circle = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 50)
.attr("cx", function(d) {
return 50 + 50 * d
})
.attr("r", 10)
.attr("fill", "tan")
.attr("stroke", "black")
.call(d3.drag()
.on("start", function(d, i, n) {
console.log(JSON.stringify(n[i]))
}))
<script src="https://d3js.org/d3.v4.min.js"></script>
PS: I'm using JSON.stringify on the D3 selection because Stack snippets freeze if you try to console.log a D3 selection.
Using "this" with an arrow function
Most of functions in D3.js accept an anonymous function as an argument. The common examples are .attr, .style, .text, .on and .data, but the list is way bigger than that.
In such cases, the anonymous function is evaluated for each selected element, in order, being passed:
The current datum (d)
The current index (i)
The current group (nodes)
this as the current DOM element.
The datum, the index and the current group are passed as arguments, the famous first, second and third argument in D3.js (whose parameters are traditionally named d, i and p in D3 v3.x). For using this, however, one doesn’t need to use any argument:
.on("mouseover", function(){
d3.select(this);
});
The above code will select this when the mouse is over the element. Check it working in this fiddle: https://jsfiddle.net/y5fwgopx/
The arrow function
As a new ES6 syntax, an arrow function has a shorter syntax when compared to function expression. However, for a D3 programmer who uses this constantly, there is a pitfall: an arrow function doesn’t create its own this context. That means that, in an arrow function, this has its original meaning from the enclosing context.
This can be useful in several circumstances, but it is a problem for a coder accustomed to use this in D3. For instance, using the same example in the fiddle above, this will not work:
.on("mouseover", ()=>{
d3.select(this);
});
If you doubt it, here is the fiddle: https://jsfiddle.net/tfxLsv9u/
Well, that’s not a big problem: one can simply use a regular, old fashioned function expression when needed. But what if you want to write all your code using arrow functions? Is it possible to have a code with arrow functions and still properly use this in D3?
The second and third arguments combined
The answer is yes, because this is the same of nodes[i]. The hint is actually present all over the D3 API, when it describes this:
...with this as the current DOM element (nodes[i])
The explanation is simple: since nodes is the current group of elements in the DOM and i is the index of each element, nodes[i] refer to the current DOM element itself. That is, this.
Therefore, one can use:
.on("mouseover", (d, i, nodes) => {
d3.select(nodes[i]);
});
And here is the corresponding fiddle: https://jsfiddle.net/2p2ux38s/

d3.js : Select this element on mouseover [duplicate]

The documentation for d3.drag states the DOM element target of the drag event will be available in this to the callback:
When a specified event is dispatched, each listener will be invoked with the same context and arguments as selection.on listeners: the current datum d and index i, with the this context as the current DOM element.
But my call back is an object instance and this points to that object. So I need another way of accessing the current DOM element that is normally passed in this. How can I do it?
Use the second and the third arguments together to get this when this is not available:
d3.drag().on(typename, function(d, i, n) {
//here, 'this' is simply n[i]
})
For a detailed explanation, have a look at the article below that I wrote to deal with this in arrow functions. The issue is different from yours, but the explanation is the same.
Here is a basic demo, try to drag a circle and look at the console:
var data = d3.range(5)
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 100);
var circle = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 50)
.attr("cx", function(d) {
return 50 + 50 * d
})
.attr("r", 10)
.attr("fill", "tan")
.attr("stroke", "black")
.call(d3.drag()
.on("start", function(d, i, n) {
console.log(JSON.stringify(n[i]))
}))
<script src="https://d3js.org/d3.v4.min.js"></script>
PS: I'm using JSON.stringify on the D3 selection because Stack snippets freeze if you try to console.log a D3 selection.
Using "this" with an arrow function
Most of functions in D3.js accept an anonymous function as an argument. The common examples are .attr, .style, .text, .on and .data, but the list is way bigger than that.
In such cases, the anonymous function is evaluated for each selected element, in order, being passed:
The current datum (d)
The current index (i)
The current group (nodes)
this as the current DOM element.
The datum, the index and the current group are passed as arguments, the famous first, second and third argument in D3.js (whose parameters are traditionally named d, i and p in D3 v3.x). For using this, however, one doesn’t need to use any argument:
.on("mouseover", function(){
d3.select(this);
});
The above code will select this when the mouse is over the element. Check it working in this fiddle: https://jsfiddle.net/y5fwgopx/
The arrow function
As a new ES6 syntax, an arrow function has a shorter syntax when compared to function expression. However, for a D3 programmer who uses this constantly, there is a pitfall: an arrow function doesn’t create its own this context. That means that, in an arrow function, this has its original meaning from the enclosing context.
This can be useful in several circumstances, but it is a problem for a coder accustomed to use this in D3. For instance, using the same example in the fiddle above, this will not work:
.on("mouseover", ()=>{
d3.select(this);
});
If you doubt it, here is the fiddle: https://jsfiddle.net/tfxLsv9u/
Well, that’s not a big problem: one can simply use a regular, old fashioned function expression when needed. But what if you want to write all your code using arrow functions? Is it possible to have a code with arrow functions and still properly use this in D3?
The second and third arguments combined
The answer is yes, because this is the same of nodes[i]. The hint is actually present all over the D3 API, when it describes this:
...with this as the current DOM element (nodes[i])
The explanation is simple: since nodes is the current group of elements in the DOM and i is the index of each element, nodes[i] refer to the current DOM element itself. That is, this.
Therefore, one can use:
.on("mouseover", (d, i, nodes) => {
d3.select(nodes[i]);
});
And here is the corresponding fiddle: https://jsfiddle.net/2p2ux38s/

D3: Separating data exit/remove/merge from drawing of elements

I am drawing some complex interactive SVGs with D3 v4 and running into some problems. My goals are:
Each data element corresponds to a group with multiple SVG shape elements (e.g. <g><circle></circle><circle></circle></g>)
The multiple SVG shape elements have to be drawn in a certain order (because they overlap)
Certain shape elements are updated without data elements being added or removed (e.g. when clicking on a shape, change the shape color)
I am running into trouble because the .data() -> .exit().remove() -> .enter() -> .merge() process requires a specific order and that order conflicts with the necessary draw order as well as the ability to update styles on the fly. This is what I started with, which does not work because of draw order:
function updateGraph() {
let eachNodeG = allNodesG
.selectAll('.eachNodeG')
.data(graphData._nodes, function (d) {
return d._id;
})
eachNodeG.exit().remove();
let eachNodeGEnter = eachNodeG.enter()
.append('g')
.attr("class", "eachNodeG")
eachNodeGEnter
.append('circle')
.classed('interactivecircle', true)
.on('click', function (d) {...})
let eachNodeG = eachNodeGEnter
.merge(eachNodeG)
.style('fill', function (d) {...}) //this is here b/c it needs to change
// when data change (without them being added/removed)
// this must be separate because the background circle needs to change even
// when nodes are not added and removed; but this doesn't work here because
// the circle needs to be in the background
eachNodeG
.append('circle')
.classed('bgcircle', true)
}
I thought maybe I could separate the data update process from the data drawing process entirely, by doing enter() exit() merge() just on the groups containing the data and then drawing everything afterward. But here I run into a different problem: either I remove and re-add all of the shapes on every update (which makes double-clicking difficult and seems like a waste of processing power), or I have to figure out some way to update only the shapes that have changed. Does it using the remove and re-add method looks like this:
// add/remove individual groups based on updated data
let eachNodeG = allNodesG
.selectAll('.eachNodeG')
.data(graphData._nodes)
eachNodeG.exit().remove();
let eachNodeGEnter = eachNodeG.enter()
.append('g')
.attr("class", "eachNodeG")
eachNodeG = eachNodeGEnter
.merge(eachNodeG)
// draw (or remove and re-draw) elements within individual groups
d3.selectAll('.bgcircle').remove()
eachNodeG.append('circle')
.classed('bgcircle', true)
d3.selectAll('.interactivecircle').remove()
eachNodeG.append('circle')
.classed('interactivecircle', true)
.style('fill', function (d) {...})
.on('click',function(d){...})
})
Is there a better way to draw the shapes in order while keeping them updateable?
You could use selection.raise or selection.lower to move circles after they have been created.

how to control the simulation speed of d3 force layout

I am following a d3 force laytout example like this.
I want to control the speed of the dots flying to the cluster. In other words, I want some dots take more time to get to their final positions, while some dots take less time.
I tried to add a timer function to control the time of each tick, but it did not work.
this.force = d3.layout.force()
.on("tick", setTimeout(tick(d), 50));
I need help for this.
Don't set a timer to call the tick function, this is done automatically by the force layout.
There are however a number of parameters you can set to modify the behaviour of the force layout. The ones most relevant to what you're trying to do are the following.
.friction() corresponds to how quickly the velocity decays and therefore directly controls how fast nodes move. The default is 0.9, to make everything slower, set it to a lower value.
.charge() controls how strong the attraction/repulsion between nodes is. This doesn't control the velocity directly, but affects it.
Of these parameters, only the latter can be set on a node-by-node basis. This makes achieving what you want a bit tricky, as you would have to carefully balance the forces. As a start, setting the charge of the nodes that you want to move slower closer to 0 should help.
There are a few other parameters of the force layout that I think would not be useful in your particular case (I'm thinking of .linkStrength() and .linkDistance()), but you may want to have a look nevertheless.
I came to a possible solution. I can just manually call tick function for each node, say 100 times, and record the path. Then I use a timer function to redraw the node according to the path. In this way, I can control the time of drawing for each node. Does this make sense?
Change the setIntervel function , intervel timing
Try this code:
Fiddle:
setInterval(function(){
nodes.push({id: ~~(Math.random() * foci.length)});
force.start();
node = node.data(nodes);
node.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 8)
.style("fill", function(d) { return fill(d.id); })
.style("stroke", function(d) { return d3.rgb(fill(d.id)).darker(2); })
.call(force.drag);
}, 50); //change the intervel time

how to draw an array of circles in a straight line, in the bottom of the browser window

I'm trying to draw a straight line of circles using d3 at the bottom of the browser window. I am not sure how this would be accomplished. I know I could create a bunch of circles using the SVG tag, but there's probably a better way using a for loop with an array.
I would like the circles to appear in a straight line at the bottom of the browser window. I would also like the circles to fill the width of the browser window as well. Any help would be greatly appreciated.
d3 has a functional style with the concept of selectors. If you are thinking of using a loop then you are probably using the tool wrong. The functional style allows you to instead concentrate on what you want to do with each item of data instead of how to process the data. There are also a number of helper functions.
Lets take Adam's solution
d3.select('body')
We are using CSS style selectors to select one object from the DOM. In this case it is the body of the document. We can do a number of things with this selection but first we append using
append('svg')
in
d3.select('body').append('svg')
This could be written to differently if we needed to reuse these selections
var body = d3.select('body');
var svg = body.append('svg');
We can the define the attributes of the object just defined
.attr('width', width)
.attr('height', height)
Now comes the interesting bit. D3 operates by binding data to selections so add data we first need a (probably) empty selection.
.selectAll('circle')
Note the use of selectAll not select.
Adam creates an array of data with
d3.range(0, width, width/10)
This uses one of d3's helper functions which behaves like the range function found in many languages with functional support (examples of use in F# & Python)
> d3.range(5)
[0, 1, 2, 3, 4]
> d3.range(0,5)
[0, 1, 2, 3, 4]
> d3.range(4,5)
[4]
// At intervals of 2
>d3.range(0,5,2)
[0, 2, 4]
Anyway we have a list of number which gets bound using
.data()
Which returns a selection. We the define what happens within the life cycle events of this selection. Because we are only dealing with data entering we can just go
.enter()
Anything under this selection will be applied to any datum entering (which in this case will be all the elements from the list). You should be able to understand what is happening until
.attr('cx', function(d){ return d; })
What is happening hear is the attribute cx is dependent on the data from the list we supplied earlier. We can provide a function which will be executed which gets passed the datum and the index of the current item.
Using some more of the helpers D3 brings
Typically you will need to use the scale helper when using D3. This allows us to abstract out the concept of pixels and instead concentrate on a fixed range.
Slightly changing the example given by Adam. Lets say we want to show 5 evenly spaced circles at the end of the document.
We can define the data like
var data = d3.range(0, 5);
And set up a scale like
var x = d3.scale.linear()
.domain([0, data.length])
.range([0,width])
With the domain (that is the input) been 0 to the max number of our data.
.domain([0, d3.max(data)])
and the range (that is what we want to output) as been 0 up to the maximum number of pixals
.range([0,width])
The example code would then look like
var width = window.innerWidth;
var height = 100;
var data = d3.range(0, 5);
var x = d3.scale.linear()
.domain([0, data.length-1])
.range([0,width])
d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.selectAll('circle')
.data(data ).enter()
.append('circle')
.style('fill', 'red')
.attr('r', height/4)
.attr('cy', height/2)
.attr('cx', function(d){ return x(d);})
We can even change that last line to
.attr('cx', function(d, i){ return x(i);})
Whilst in this example the index and the data is the same this allows us to to space out the items whilst keeping the data simple. Say if the array actually was the values which r should be
...
var data = [4,1,20,5,7];
...
.attr('r', function(d){ return d;})
...
.attr('cx', function(d, i){ return x(i+0.5);})
var width = window.innerWidth;
var height = 100;
d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.selectAll('circle')
.data(d3.range(0, width, width/10)).enter()
.append('circle')
.style('fill', 'red')
.attr('r', height/4)
.attr('cy', height/2)
.attr('cx', function(d){ return d; })

Categories

Resources