AndroidNoobie AndroidNoobie - 4 months ago 32
Javascript Question

d3 - problems with tickFormat + mutable variable accessible from closure?

I have an array of objects that maps actual tick values to what I want the display labels of the ticks to be. In other words, the object is keyed on tick value and gives the display value.

var tickMap = [{"27": 27, "28": 28, "29": 29, "30": 30, "31": "Test", "32": "Test2", "33": 33},
{"50": 50, "58": "Test3", "66": 66, "74": 74, "82": 82, "90": 90}];


I then loop over this, and using the
tickFormat
property, try to return the display value for each tick value.
d
is the actual numeric value of the tick from
tickValue
:

for (var i = 0; i < tickMap.length; i++) {
index = i;
dimensions[i] = {
'tickFormat': function(d) {
return tickMap[index][d];
},
// other dimension properties are here, including arrays of
// the actual tick values in the tickValues property.
}
};


But this only ever seems to work for the second (or last) axis in the array. Instead of using
tickMap[i][d]
, I tried to declare
index
globally, and then assign the value of
i
to
index
, but that didn't help.

I also get the error "Mutable variable is accessible from closure".

I'm not sure how to fix this. Any help would be greatly appreciated.

Answer

Your problem is that functions in JS maintain a reference to the actual variable that was created in their closure, not the value. This means that i in your code will be the same i (same memory location) for every tickFormat function, containing the value of tickMap.length which it got when the loop finished.

ES6 Solution

If you can use ES6, just use let instead of var and everything should work because let creates a new variable for each loop iteration, meaning each tickFormat function would get its own copy of i:

for (let i = 0; i < tickMap.length; i++) {
  dimensions[i] = {
    'tickFormat': function(d) {
      return tickMap[i][d];
    }
  }
};

ES5 & bellow Solution

Otherwise, you have to do the same manually by wrapping your loop code in an Immediately Invoked Function Expression (IIFE) and passing i into it. This will create a new variable in IIFE's arguments which will, on each iteration, hold the value of i that was passed into the IIFE when it was created:

for (var i = 0; i < tickMap.length; i++) {
  dimensions[i] = (function iife(index) {
    return {
    'tickFormat': function(d) {
      return tickMap[index][d];
    };
  })(i); // <-- here you pass `i` as the `index`, maintaining a unique reference and thus the correct value for each `i`
};