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
Related
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/
I'm trying to work out what's going on in Mike Bostock's Box Plot example from the D3 gallery. Here's the code inside an Observable notebook: https://observablehq.com/#d3/box-plot
In it there's a code block that does not appear to be a function definition but that has a return value:
chart = {
const svg = d3.select(DOM.svg(width, height));
const g = svg.append("g")
.selectAll("g")
.data(bins)
.join("g");
// [...]
return svg.node();
}
What does return do or mean when it is not in a function definition?
Yep, as the commenters have suggested, this is a syntax that's particular to Observable. What you're seeing a cell that uses a block, as mentioned in the Introduction to Code.
How you can think of this relative to other JavaScript is that it's kind of like an IIFE, but with the added consideration that, if it references other cells, it automatically resolves them. So in vanilla JavaScript, this would be like:
chart = (() => {
const svg = d3.select(DOM.svg(width, height));
const g = svg.append("g")
.selectAll("g")
.data(bins)
.join("g");
// [...]
return svg.node();
})()
In fact, that's roughly what they compile to. The particular syntax is that way because it's meant to be clear that it's code that runs when references change - see how Observable runs for details on that. Unlike an IIFE, a cell in Observable might run multiple times, if something that it references, like a generator or Promise, changes.
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/
I'm trying to work out what's going on in Mike Bostock's Box Plot example from the D3 gallery. Here's the code inside an Observable notebook: https://observablehq.com/#d3/box-plot
In it there's a code block that does not appear to be a function definition but that has a return value:
chart = {
const svg = d3.select(DOM.svg(width, height));
const g = svg.append("g")
.selectAll("g")
.data(bins)
.join("g");
// [...]
return svg.node();
}
What does return do or mean when it is not in a function definition?
Yep, as the commenters have suggested, this is a syntax that's particular to Observable. What you're seeing a cell that uses a block, as mentioned in the Introduction to Code.
How you can think of this relative to other JavaScript is that it's kind of like an IIFE, but with the added consideration that, if it references other cells, it automatically resolves them. So in vanilla JavaScript, this would be like:
chart = (() => {
const svg = d3.select(DOM.svg(width, height));
const g = svg.append("g")
.selectAll("g")
.data(bins)
.join("g");
// [...]
return svg.node();
})()
In fact, that's roughly what they compile to. The particular syntax is that way because it's meant to be clear that it's code that runs when references change - see how Observable runs for details on that. Unlike an IIFE, a cell in Observable might run multiple times, if something that it references, like a generator or Promise, changes.
I'm trying to work out what's going on in Mike Bostock's Box Plot example from the D3 gallery. Here's the code inside an Observable notebook: https://observablehq.com/#d3/box-plot
In it there's a code block that does not appear to be a function definition but that has a return value:
chart = {
const svg = d3.select(DOM.svg(width, height));
const g = svg.append("g")
.selectAll("g")
.data(bins)
.join("g");
// [...]
return svg.node();
}
What does return do or mean when it is not in a function definition?
Yep, as the commenters have suggested, this is a syntax that's particular to Observable. What you're seeing a cell that uses a block, as mentioned in the Introduction to Code.
How you can think of this relative to other JavaScript is that it's kind of like an IIFE, but with the added consideration that, if it references other cells, it automatically resolves them. So in vanilla JavaScript, this would be like:
chart = (() => {
const svg = d3.select(DOM.svg(width, height));
const g = svg.append("g")
.selectAll("g")
.data(bins)
.join("g");
// [...]
return svg.node();
})()
In fact, that's roughly what they compile to. The particular syntax is that way because it's meant to be clear that it's code that runs when references change - see how Observable runs for details on that. Unlike an IIFE, a cell in Observable might run multiple times, if something that it references, like a generator or Promise, changes.