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

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/

Related

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.select(this) works on mouseover, but not on function called in mouseover

I am new to javascript and currently struggling with selecting the this object while trying to do a d3 selection. I've made the following example, with a function I'm calling, and an on mousemove event:
function changeFont() {
d3.select(this)
.attr('font-size', '2em')
}
...
.on('mousemove', function() {
var mouse = d3.mouse(this);
var xVal = mouse[0];
// this would work, but not when its called in a function
// d3.select(this)
// .attr('font-size', '2em')
// this works
d3.select(this)
.attr("opacity", 1)
// this doesnt
changeFont()
});
In my main script not shown here, I am organizing my code by writing functions that handle each of the mousemove, mouseover, etc. effects. However because of these functions, I am running into this problem where I can't do d3.select(this) inside of that mouseover function... Any thoughts on what I should be doing differently?
Should I pass this as a parameter to my changeFont() function? Or should I access this in a different way?
Thanks!
Although Andrew's answer might be the best fit if you take the question literally, I would like to add my two cents to it. Your real problem does not seem to be to get a hold of this, but to repeatedly get access to that element to apply you manipulations. Since fiddling around with this can be a pain in JavaScript it might be worth taking a slightly different approach by directly passing the selection instead. This will also improve performance as there is no need to re-select this over and over again.
First, let us slightly refactor your changeFont() function to accept a selection object.
function changeFont(selection) {
selection
.attr('font-size', '2em');
}
Note, how this makes the function more generally applicable as it does not make any assumptions about the selection passed into it. It could be your d3.select(this), a selection containing multiple elements or any other D3 selection object. Additionally, you do not need to preserve the previous this scope.
There are basically two ways of calling this function.
The obvious one will directly pass the selection as an argument when calling the function:
const d3This = d3.select(this);
changeFont(d3This);
Fortunately, there is a more elegant way of doing it by resorting to D3's own selection.call() which even allows for method chaining if you need to do multiple calls on the same selection.
function changeFont(selection) { selection.attr("font-size", "2em"); }
function changeFill(selection) { selection.attr("fill", "limegreen"); }
function changeOpacity(selection) { selection.attr("opacity", "0.1"); }
// ...
.on("mouseover", function() {
// Call the functions for this element.
d3.select(this)
.call(changeFont)
.call(changeFill)
.call(changeOpacity);
// Instead, you could also apply the same pattern to all texts.
d3.selectAll("text")
.call(changeFont)
.call(changeFill)
.call(changeOpacity);
}
Just for completeness, since this question has already two very good answers: you can avoid the confusion with this if you use the third and second arguments combined. That's something that even D3 developers forget eventually.
In several D3 methods, the current DOM element is just the current index of the nodes' group. So, in the anonymous function...
.on("mouseover", function(_, i, n) {
... this is just n[i], which you can just pass to the other functions. The _ here corresponds to the first argument, the datum: I'm using _ just to follow the convention that shows this argument is not used.
The nice thing about this approach is that you can even (for whatever reasons you have) use arrow functions:
d3.select("body").selectAll(null)
.data(["foo", "bar", "baz"])
.enter()
.append("p")
.text(String)
.on("mouseover", (_, i, n) => {
changeFont(n[i])
});
function changeFont(element) {
d3.select(element).style("font-size", "2em")
}
<script src="https://d3js.org/d3.v5.min.js"></script>
Of course, you can't get the DOM element using this inside the arrow function.
Let's see what this is defined as for each of your approaches:
// console.log(this) in inline function:
<svg width="960" height="960">
// console.log(this) in function called from inline function:
Window → file:///fileName.html
this is set by how a function is called. D3 conveniently sets this to be the DOM element being manipulated by using .apply on the function passed to selection.attr(), selection.on() etc. However, it doesn't do this for functions called within the function passed to selection.attr(), selection.on(), etc.
We can see that this is indeed the DOM element if we log this in the function passed to selection.on(). If this is not explicitly set, it will be the window (unless using strict mode, then it will be undefined). We can see in the nested function, this is indeed the window.
Altocumulus's answer and Gerardo's answer avoid the this issue altogether, additionally you could also pass this as some regular argument to the function (this pattern is seen in some examples). But if you want to simply be able to copy and paste the code from the inline function to some separately defined function you can use apply, which will preserve this as the element being modified:
d3.select("body")
.append("svg")
.attr("width", 960)
.attr("height", 960)
.on('mousemove', function() {
var mouse = d3.mouse(this);
console.log("inline: ", mouse[0]);
someFunction.apply(this);
});
function someFunction() {
var mouse = d3.mouse(this);
console.log("not inline: ", mouse[0]);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
If you needed to pass parameters to your function along with this, you can still use apply:
someFunction.apply(this,[parameterA,parameterB,...]);
function someFunction(parameterA,parameterB) { }
d3.select("body")
.append("svg")
.attr("width", 960)
.attr("height", 960)
.on('mousemove', function() {
var mouse = d3.mouse(this);
console.log("inline: ", mouse[0]);
someFunction.apply(this,[mouse[0],mouse[1]]);
});
function someFunction(x,y) {
var mouse = d3.mouse(this);
console.log("not inline: ", mouse[0],x,y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
However, this inline function calling other functions may just be extra work. If you are only calling a function in the inline function, then just pass the called function to selection.on() directly, this preserves this without any extra steps as d3 will apply the expected value to it (it also still gives you access to the datum and index if needed):
d3.select("body")
.append("svg")
.attr("width", 960)
.attr("height", 960)
.on('mousemove', someFunction)
function someFunction() {
var mouse = d3.mouse(this);
console.log(mouse[0]);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Don't place the brackets on the function in this case, we don't want to return the result of the function, we want to use the function itself.
I've use apply (Function.prototype.apply()) in my examples, but you can also use call (Function.prototype.call()), as Altocumulus notes below. The use of call is quite similar. If you aren't passing any parameters to the function and only want to preserve this, the usage is the same: someFunction.apply(this) / someFunction.call(this). But, if passing parameters, call doesn't use an array for the parameters:
d3.select("body")
.append("svg")
.attr("width", 960)
.attr("height", 960)
.on('mousemove', function() {
var mouse = d3.mouse(this);
someFunction.call(this,mouse[0],mouse[1]); // first parameter will be `this` for someFunction, rest of parameters follow
});
function someFunction(x,y) {
var mouse = d3.mouse(this);
console.log(mouse[0],x,y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

How to use 'this' in Angular with D3?

Tldr; How do I deal with this in reference to a D3 object when Angular binds this to the class (component/service)?
I am looking to use D3.js (v.4) in an Angular (v.4) app.
My code works in standalone JavaScript but I now need to integrate it into an Angular app.
The use of this is tripping me up.
I have an SVG group that I wish to drag and so I use .call(drag)
someFunction() {
this.unitGroup = this.svg.append('g')
.attr('id', 'unitGroup');
.call(drag)
}
My problem comes about when I try to reference the svg element that is being dragged. In my original code, I could refer to this e.g. let matrix = this.getCTM(). Using this is now not working when using this code within a service.
drag = d3.drag()
.on('start', () => {
this.setClickOrigin(d3.event);
})
.on('drag', (d) => {
const m = this.getCTM(); // <--- PROBLEM HERE
const x = d3.event.x - this.clickOrigin.x;
const y = d3.event.y - this.clickOrigin.y;
this.setClickOrigin(d3.event);
d3.select(this) // <--- PROBLEM HERE
.attr('transform', `matrix(${m.a},${m.b},${m.c},${m.d},${m.e + x},${m.f + y})`);
});
Any pointers on how to implement this or clarification of what I am doing wrong would be appreciated.
I don't think this is simply an error associated with the arrow function this binding as .on('drag', function(d){...} results in the same error.
Here is Plunker illustrating my issue: https://embed.plnkr.co/p1hdhMBnaebVPB6EuDdj/
In most of D3 methods, this refers to the DOM element, and it is the most simple way to get the element. However, you're facing some problems using this in your angular code.
The good news is that there is an idiomatic way to get the current DOM element without relying on this (and without relying on d3.event as well): using the second and the third arguments combined. This is quite useful in situations where you cannot use this, like your situation right now or when using an arrow function, for instance.
That alternative to this is extensively documented on the API. For most D3 methods, you can read that the method is...
... being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element (nodes[i]). (both emphases mine)
So, in a common D3 code, you can get the DOM element using:
.on("whatever", function(){
d3.select(this).etc...
// ^--- 'this' is the DOM element
Or:
.on("whatever", function(d,i,n){
// ^-^--- second and third arguments
d3.select(n[i]).etc...
// ^--- here, 'n[i]' is also the DOM element
Therefore, in your case, just use:
.on('drag', (d,i,n) => {
const m = d3.select(n[i]).node().getCTM();
//the same of 'this'-----^
...
}
Since d3.select(n[i]) is a selection, you'll have to use node() to get the actual DOM element.
Here is your updated plunker: https://plnkr.co/edit/tjQ6lV411vAUcEKPh0ru?p=preview
Try using d3.event.sourceEvent.target:
.on('drag', () => {
const target = d3.event.sourceEvent.target;
const m = target.getCTM();
const x = d3.event.x - this.clickOrigin.x;
const y = d3.event.y - this.clickOrigin.y;
this.setClickOrigin(d3.event);
d3.select(target)
Forked Plunker Example

Basic d3: why can you select things that don't exist yet?

I've been learning about d3, and I'm a bit confused about selecting. Consider the following example:
http://bl.ocks.org/mbostock/1021841
Specifically, let's look at this line:
var node = svg.selectAll(".node")
.data(nodes)
.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, i) { return fill(i & 3); })
.style("stroke", function(d, i) { return d3.rgb(fill(i & 3)).darker(2); })
.call(force.drag)
.on("mousedown", function() { d3.event.stopPropagation(); });
In the documentation it says, "A selection is an array of elements pulled from the current document." I interpret this to mean that svg.selectAll(.node) creates an array of elements of class .node pulled from the current document, but as far as I can tell there are no such elements! Unless I'm confused - and I'm almost certain that I am - the only place in the document where something is given the class "node" is after the selection has already occurred (when we write .attr("class", "node")).
So what is going on here? What does svg.selectAll(".node") actually select?
Although, at first sight, this may look like a simple and silly question, the answer to it is probably the most important one for everyone trying to do some serious work with D3.js. Always keep in mind, that D3.js is all about binding data to some DOM structure and providing the means of keeping your data and the document in sync.
Your statement does exactly that:
Select all elements having class node. This may very well return an empty selection, as it is in your case, but it will still be a d3.selection.
Bind data to this selection. Based on the above mentioned selection this will, on a per-element basis, compute a join checking if the new data is a) not yet bound to this selection, b) has been bound before, or c) was bound before but is not included in the new data any more. Depending on the result of this check the selection will be divided into an enter, an update, or an exit selection, respectively.
Because your selection was empty in the first place. All data will end up in the enter selection which is retrieved by calling selection.enter().
You are now able to append your new elements corresponding to the newly bound data by calling selection.append() on the enter selection.
Have a look at the excellent article Thinking with Joins by Mike Bostock for a more in-depth explanation of what is going on.

d3 Force: Making sense of data binding

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.

Categories

Resources