Responding to D3 events in TypeScript

This article refers to TypeScript 1.x and is now outdated.

Introduction

D3 offers a great way of interacting with created DOM elements by responding to various events. Binding to events and intercepting data is easy enough. In native JavaScript, the element firing the event can directly be accessed via the this keyword. However, TypeScript has its own way of dealing with this that makes the interaction trickier. In this article, I will try guiding you to a way of responding to events with TypeScript.

This article assumes you have a bit of knowledge about D3, in particular selections. Examples will be built on top of the chart used in previous articles (code available here).

Responding to events

Responding to an event in D3 is extremely simple. Assuming you have a selection, binding to events on that selection is as simple as adding .on(eventName, callback(d,i)) where eventName is the name of any DOM event, and callback(d,i) is a callback function where d is the current data of the selection and i is the index of the data in the selection. To make it clearer, let’s take an example (charting.ts in the code):

var dataSelection = this._dataGroup
    .selectAll('.post')
    .data(data);

dataSelection.enter()
    .append('circle')
    .classed('post', true)
    .on('mouseover', (d, i) => {
        console.log(d);
    });

Now, if you open your console and place your mouse over one of the points displayed, the console will log the data you just hovered on. Great!

Making it more interesting

Now that we know when an element is hovered, let’s say you want to change the background color of the hovered element. To do that, all examples you will see will tell you to do something like:

dataSelection.enter()
    .append('circle')
    .classed('post', true)
    .on('mouseover', function(d, i){
        d3.select(this)
          .style({
              'fill':'yellow'
          });
    });

This would work fine in JavaScript, but this is actually invalid TypeScript. If you try to compile this code, you will get an error telling you that this is not assignable to parameter of type ‘EventTarget’.
But why

The problem here is that TypeScript recognizes this as an instance of the current class, whatever the context. To be able to handle the callback according to that principle, the TypeScript compiler actually initializes a _this variable before the callback, to keep a reference to the original this (emitted JavaScript):

var _this = this;
dataSelection.enter()
    .append('circle')
    .classed('post', true)
    .on('mouseover', function (d, i) {
        d3.select(_this).style({
            'fill': 'yellow'
            });
        });

To work around that problem, I use to access the original event emitter explicitly via d3.event.currentTarget:

dataSelection.enter()
    .append('circle')
    .classed('post', true)
    .on('mouseover', (d, i) => {
        d3.select(d3.event.currentTarget).style({
            'fill': 'yellow'
            });
        });

And there you are! Note that this code will set the fill color, but won’t reset it when the mouse gets out. I leave this as an exercise to the reader.
mouseover

Calling a method

Let’s now imagine that you want to refactor your code for better readability, by extracting the callback method:

//update method
update(data: Array<dataPoint>) {
    //code omitted for brievety
    var dataSelection = this._dataGroup
        .selectAll('.post')
        .data(data);

    dataSelection.enter()
        .append('circle')
        .classed('post', true)
        .on('mouseover', this.onMouseover);

    //code omitted for brievety
}

//mousover callback
private onMouseover(d, i) {
    d3.select(d3.event.currentTarget).style({
        'fill': 'yellow'
    });
}

This works perfectly fine, and does what you expect it to do. Now, let’s expand the callback to also log the data via a private method:

private onMouseover(d, i) {
    d3.select(d3.event.currentTarget).style({
        'fill': 'yellow'
    });
    this.logData(d);
}

private logData(d: any) {
    console.log(d);
}

…and it’s not working anymore, as this is not what you think it is.
What?

As we are inside the event callback, this now represents the selection emitting the event. TypeScript won’t, in this case, replace this by _this, because, well, why would it? This is just a function body.

To circumvent the problem, the solution is simple:

update(data: Array<dataPoint>) {

    //code omitted for brievety
    var dataSelection = this._dataGroup
        .selectAll('.post')
        .data(data);

    dataSelection.enter()
        .append('circle')
        .classed('post', true)
        .on('mouseover', this.onMouseover());
    //code omitted for brievety
}

private onMouseover(): (d, i) => void {
    return (d, i) => {
        d3.select(d3.event.currentTarget).style({
            'fill': 'yellow'
        });
        this.logData(d);
    }
}

private logData(d: any) {
    console.log(d);
}

And now it works again. What happens here is that a reference to the original this will be kept in the onMouseover method, as a function is returned by the method. The result function is what is used as the callback for the event. Emitted JavaScript looks as follows:

chart.prototype.onMouseover = function () {
    var _this = this;
    return function (d, i) {
        d3.select(d3.event.currentTarget).style({
            'fill': 'yellow'
        });
        _this.logData(d);
    };
};

When the callback is registered, the onMouseover method is called. Therefore, any time the event callback will be called, it will be using _this instead of this, which will be working fine.

Conclusion

Events in D3 are, according to me, one of the place where the use in TypeScript gets tricky. Managing to understand what is going on and how the this variable is handled by TypeScript and JavaScript is a corner stone. I hope this articles helped you understand how to join the ends, or at least how to get out of some troubles and get around some problems you might be facing.

Leave a comment

20 Comments

  1. 32

     /  October 12, 2016

    Excellent!

    Like

    Reply
  2. Thesa

     /  February 21, 2017

    Hey, thanks a lot for the great explanation. Have you used the selection.each function of D3 in TypeScript? I’m trying to integrate this line wrap example (https://bl.ocks.org/mbostock/7555321) in one of my charts. But I have the problem that I cannot use ‘this’ inside the .each() anonymous function.

    Like

    Reply
    • Do you use an actual function (i.e. not an arrow function)? I thought typings had gotten better at handling this kind of situation. The nasty solution that always works is to cast `this` to type so that you won’t get a complaint from the compiler

      Like

      Reply
  3. tanguy

     /  April 3, 2017

    Thanks. I didn’t know how to handle this “this” mess

    Like

    Reply
  4. david Nelson

     /  May 15, 2017

    Thanks for the post. “this” is a bit confusing here.

    I did all you recomended and my code is working fine except:

    I get these two compile errors”

    ERROR in ./src/app/vmesa/flowsheet/vmesa.flowsheet.component.ts
    (739,46): error TS2339: Property ‘currentTarget’ does not exist on type ‘Event | BaseEvent’.

    ERROR in ./src/app/vmesa/flowsheet/vmesa.flowsheet.component.ts
    (739,103): error TS2339: Property ‘getAttribute’ does not exist on type ‘EventTarget’.

    line 739 in my click event handler is:
    let rawGuid = d3.select(d3.event.currentTarget).select(‘[v\\:nameu=”SourceGUID”]’).node().getAttribute(‘v:val’);

    All works fine. Just want the compile error to go away 🙂 Guessing maybe my d3 defs area little old?

    Like

    Reply
    • If your intent is simply to get rid of the error message, you can cast `d3.event` to type `any“, and do something like `d3.select((d3.event as any).currentTarget)`. This should help.

      Not sure if the problem comes from the defs age, or with type inference that cannot determine whether type is Event or BaseEvent…

      Like

      Reply
      • David Nelson

         /  May 26, 2017

        Thanks I was just trying to make the error go away

        Like

  5. webdev

     /  May 23, 2017

    This article is outdated, as typescript ^2.0 does not let you define executing functions as callbacks, so no scope caching possible.

    Like

    Reply
  6. Thanks, this was a life-saver!

    Like

    Reply
  7. ted

     /  June 5, 2020

    hi..I have a query..i want to populate three drill down chart.but the third chart function is not accessible.
    Code :
    ngoninit()
    {
    this.renderchart();
    }
    renderchart()
    {
    ->all svg codes and onclick
    .on(‘click’, this.Secondchart)
    }
    Secondchart()
    {
    -.> all svg codes for the the chart3
    .on(‘click’, this.Thirdchart)
    }
    Thirdchart()
    {
    }

    Runtimeerror – Thirdchart is not a recognized function

    Like

    Reply
  1. 1 – Responding to D3.js events in TypeScript - Exploding Ads
  2. Making a D3 chart responsive | Wandering in the community
  3. Creating a chart with D3 and TypeScript – part 3 | Wandering in the community

Leave a comment