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’.
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.
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.
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.
32
/ October 12, 2016Excellent!
LikeLike
huguesstefanski
/ October 14, 2016Glad you enjoyed!
LikeLike
Thesa
/ February 21, 2017Hey, 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.
LikeLike
huguesstefanski
/ February 21, 2017Do 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
LikeLike
Anthony Stark
/ March 14, 2017Thanks!
LikeLike
huguesstefanski
/ March 16, 2017Most welcome!
LikeLike
tanguy
/ April 3, 2017Thanks. I didn’t know how to handle this “this” mess
LikeLike
huguesstefanski
/ April 6, 2017Thanks, happy I could help!
LikeLike
david Nelson
/ May 15, 2017Thanks 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?
LikeLike
huguesstefanski
/ May 17, 2017If 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…
LikeLike
David Nelson
/ May 26, 2017Thanks I was just trying to make the error go away
LikeLike
webdev
/ May 23, 2017This article is outdated, as typescript ^2.0 does not let you define executing functions as callbacks, so no scope caching possible.
LikeLike
huguesstefanski
/ May 26, 2017Was not aware of this, I will add a note at the top of the article! Thanks
LikeLike
Anthony Saliba
/ June 9, 2018Thanks, this was a life-saver!
LikeLike
huguesstefanski
/ June 10, 2018Most welcome!
LikeLike
ted
/ June 5, 2020hi..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
LikeLike
huguesstefanski
/ June 14, 2020Is the complete code available on GitHub? It’s hard to provide a response without a global context…
LikeLike