4

I currently started playing with the d3 framework. I observed that for v3 there is a third parameter passed to the event listener. It seems to be always 0 but I cannot find any information on what it is suppose to represent?

From the docs:

The specified listener is invoked in the same manner as other operator functions, being passed the current datum d and index i, with the this context as the current DOM element.

What is the meaning of this third parameter?


In this example you will see that once you click any of the rectangles 3 parameters are passed to f:

var svg = d3.select("body").append("svg")

function f(d, idx, whoami) {
    console.log('I am the data:\t' + d);
    console.log('I am the index:\t' + idx);
    console.log('But who am i?\t' + whoami);
    console.log('Length:' + arguments.length);
    console.log(arguments);
}

var data = ['A', 'B', 'C'];

svg.selectAll('rect')
  .data(data)
    .enter()
    .append('rect')
    .attr("x", 0)
    .attr("y", function(el, i) {return i * 40;})
    .attr("width", 100)
    .attr("height", 40)
    .on("click", f);
rect {
  fill: #333;
  opacity: 0.3;
  stroke: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
thothal
  • 16,690
  • 3
  • 36
  • 71

1 Answers1

3

That is actually an undocumented feature. Looking at the implementation of selection.on() back in v3.5.17 you will notice that selection.each() is used internally to bind the listener to each node of the current selection.

return this.each(d3_selection_on(type, listener, capture));

From there on the internal function d3_selection_on will evaluate to onAdd for the addition of listeners and the reference to the onAdd function will be passed as the callback to .each(). Therefore, .each() will actually execute onAdd for every node in the current selection. onAdd, on the other hand, will close over the arguments passed to it by the .each() call whereby storing them in the listener's context:

function d3_selection_on(type, listener, capture) {

  /* ... */

  function onAdd() {
    var l = wrap(listener, d3_array(arguments));  // wrap points to d3_selection_onListener
    /* ... */
  }

  /* ... */

function d3_selection_onListener(listener, argumentz) {
  return function(e) {
    /* ... */
    argumentz[0] = this.__data__;                 // Set the latest datum as the first argument
    try {
      listener.apply(this, argumentz);            // Pass arguments from closure to the listener
    } finally {
      d3.event = o;
    }
  };
}

Looking at the implementation of selection.each() one notices that not just two arguments—as mentioned in the docs—are passed to the callback but rather three arguments:

if (node = group[i]) callback(node, i, j);

The third argument j being the group's index. Since your code does not make use of grouping that argument always evaluates to 0.

Modifying your demo one can easily demonstrate how setting up a grouped selection affects the value of that third argument. The following snippet duplicates your three rects putting each set of rects into one <g> while establishing a grouping using a sub-selection with its own data binding.

var svg = d3.select("body").append("svg")

function f(d, idx, whoami) {
    console.log('I am the data:\t' + d);
    console.log('I am the index:\t' + idx);
    console.log('But who am i?\t' + whoami);
    console.log('Length:' + arguments.length);
    console.log(arguments);
}

var data = [['A', 'B', 'C'], ['a', 'b', 'c']];

svg.selectAll('g')
  .data(data)
  .enter().append('g')
  .selectAll('rect')
  .data(d => d)
  .enter().append('rect')
    .attr("x", function(el, i, j) {return j * 110;})
    .attr("y", function(el, i) {return i * 40;})
    .attr("width", 100)
    .attr("height", 40)
    .on("click", f, true);
rect {
  fill: #333;
  opacity: 0.3;
  stroke: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>

Again, this is totally undocumented as it is mentioned neither in the documentation on .each() nor in the one on .on().

altocumulus
  • 21,179
  • 13
  • 61
  • 84
  • 2
    Sometimes I think D3 docs are too superficial... But then I see some libraries with their so-called docs so deficient that I change my mind, D3 docs are quite good. – Gerardo Furtado Jan 26 '21 at 21:42
  • 2
    @GerardoFurtado Exactly. I am always surprised how good the D3 documentation actually is. At least, if compared to others. Honestly, I think it is one of the best documentations of free software I have ever come across. It's not perfect, still most questions can be answered by reading the docs. U4D – altocumulus Jan 26 '21 at 22:01
  • 1
    Agree with both of you, I'm always a bit annoyed when I see questions here saying the docs are horrible when they tend to answer that same question. Especially when compared with most other libraries that seem way worse. The docs are even more impressive if you were to combine each module's docs into one document, it's a huge effort. @altocumulus, nice work tracking down the answer. – Andrew Reid Jan 26 '21 at 23:03
  • 1
    @AndrewReid There is an interesting development that Fil pointed out to me in a [comment](https://github.com/d3/d3/issues/3452#issuecomment-727053326) on an issue I opened on the GitHub repo: some work is in progress to make the API.md files machine readable! See demo applications at https://observablehq.com/d/3892df5ed9d82978 and https://observablehq.com/d/352e015b493a0a84. That is so awesome! – altocumulus Jan 27 '21 at 01:30
  • So it is the group ID. Then it actually _is_ documented indirectly in [`dispatch`](https://github.com/d3/d3-selection#selection_dispatch). – Sebastian Simon Jan 27 '21 at 14:12
  • @SebastianSimon No, that's a totally different thing. Firstly, the documentation you linked to is the one for the latest version 6. For v3 things were a bit different: https://github.com/d3/d3-3.x-api-reference/blob/master/Internals.md#d3_dispatch. Secondly, the docs are talking about the *nodes*, i.e. the group's contents, whereas the third parameter gets the group's index, i.e. the index of the group itself. Thirdly, `d3.dispatch` is not involved in the entire call stack for this piece of code. – altocumulus Jan 27 '21 at 16:01
  • Funnily enough, some @GerardoFurtado guy (not sure if I have heard of him before, seems to be quite into D3, though) posted an interesting insight into this issue some 4 and a half years ago: [*"d3.js v4: How to access parent group's datum index?"*](/q/38233003). This sheds some more light on case. – altocumulus Jan 27 '21 at 16:06