15

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/

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
Neil Docherty
  • 555
  • 4
  • 20
  • Possible duplicate of [What does "this" refer to in arrow functions in ES6?](https://stackoverflow.com/questions/28371982/what-does-this-refer-to-in-arrow-functions-in-es6) – str Sep 25 '17 at 13:20
  • changing to on('drag', function (d) { ... } ) doesn't solve the issue. – Neil Docherty Sep 25 '17 at 13:23
  • Can you reproduce it in plunker? You can try changing `this` to `drag` like `d3.select(drag)` Or maybe you are looking for `d3.event.currentTarget` instead of `this` – yurzui Sep 25 '17 at 13:25
  • I'll have a look at doing that. I was hesitant to do so at first due to the amount of code required and I was hoping that someone would recognize my error. – Neil Docherty Sep 25 '17 at 13:28
  • See also related question https://stackoverflow.com/questions/39350774/angular2-d3-update-d3-svg-mouse-pos-to-component-property – yurzui Sep 25 '17 at 13:29
  • `.on('drag', (function(d){...}).bind(drag))` – Castro Roy Sep 25 '17 at 13:39
  • @Castro Roy I suspect that `this.clickOrigin.x` and `this.setClickOrigin` are component properties while others should be current instance of d3 object – yurzui Sep 25 '17 at 13:41
  • @yurzui, that's right – Neil Docherty Sep 25 '17 at 13:44
  • So have you tried `d3.event.currentTarget` or `drag` instead of `this` for instance of d3 object? – yurzui Sep 25 '17 at 13:45
  • Yeah, console.log(d3.event.currentTarget) returns undefined. In my original code, it would return the svg group being dragged. – Neil Docherty Sep 25 '17 at 13:46
  • 1
    I want to see it in a plunker) Here is my example of d3 https://plnkr.co/edit/qM3qrk3swvalQFBh1Db1?p=preview – yurzui Sep 25 '17 at 13:48
  • Thanks for the example, I'll take a look at that and create plunker. – Neil Docherty Sep 25 '17 at 14:02
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/155258/discussion-between-neil-docherty-and-yurzui). – Neil Docherty Sep 25 '17 at 15:07

2 Answers2

15

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

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • Great! Thank you! The Plunker works as intended. To get this to work in my code I had to make a minor adjustment to get a typing error to pass - Property 'getCTM' does not exist on type 'Element'. const m = d3.select(n[i] as any).node().getCTM(); – Neil Docherty Sep 26 '17 at 08:42
  • Interesting. How about when `d3` is declared in my `AppComponent`, how do you reference `d3` without stating `this.d3`? In my case, I created the canvas on ngOnInit of AppComponent and I can't seem to refer to d3 without stating `this.d3` – Michael 'Maik' Ardan Mar 07 '18 at 10:56
  • @MichaelArdan Sorry, I'm not an angular user. – Gerardo Furtado Mar 07 '18 at 11:01
  • Very helpful thank you for `this` - Using angular and d3 together in a programmatic way is really a pain when it comes to stuff like this. – Dennis Ich Jan 04 '19 at 11:53
  • Great Thanks! I was also facing same issue for area graph, but this approach solved my problem. – Arup Garai Feb 10 '20 at 20:13
2

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

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Thanks for the solution! I've marked the answer from @GerardoFurtado as accepted as it seems more robust. When moving the dark-grey box in your solution, the event sometimes aprpears to switch to the larger box and then move that instead. – Neil Docherty Sep 26 '17 at 08:39
  • It solved one of them! I'm grateful for the help. Bonus points available in the next problem: https://stackoverflow.com/questions/46423055/d3-zoom-event-firing-on-drag-in-angular – Neil Docherty Sep 26 '17 at 09:48