1

I am running D3 in a loop. I would like to pass variable 'i' in a D3 function, but I am unable to do so. Here is my code -can you please tell me what I am doing wrong?

works perfectly fine

.style("fill", dataz[i].color) 

does not work

.style("fill-opacity", function(d, i) {
            if (dataz[i].color === "#ffff") {
              return "0";
            });

full code

  for (i = 0; i < dataz.length; i++) {
    d3.select(list[i])
      .style("fill", dataz[i].color)
      .style("fill-opacity", function(d, i) {
        if (dataz[i].color === "#ffff") {
          return "0";
        } else {
          return "0.5"
        }
      })
  }
Noobster
  • 1,024
  • 1
  • 13
  • 28

2 Answers2

2

As a general rule, you don't need loops when using D3. It's (normally) completely unnecessary, given what D3 does with the data. It's true that we do use loops sometimes in a D3 dataviz, but only in very specific situations, to solve very specific problems. For the majority of situations, when we see a D3 code using a loop, we tend to think that that code is not using D3 capabilities correctly (also, in your code, you're mixing the i in the for loop with the i in the anonymous function. They are not the same).

So, to access your data array using the second argument, which is the index, forget about for loops: you just need to pass it in your selection.

Here is a simple example, using an enter selection. We check if the dataz value is blue. If it is, we set the opacity to 0.1:

var svg = d3.select("svg");

var dataz = ["teal", "brown", "blue", "purple"];

var circles = svg.selectAll(".circles")
    .data(dataz)
    .enter()
    .append("circle");

circles.attr("r", 20)
    .attr("cy", 50)
    .attr("cx", (d, i) => i * 50 + 40)
    .attr("fill", d => d)
    .attr("stroke", "black")
    .style("fill-opacity", (d, i) => dataz[i] === "blue" ? 0.1 : 1)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

Pay attention to this: in my snippet above, dataz[i] is the same of d (the datum itself).

Explaining the problem with your code:

The i in the anonymous function is not the i in the loop. Due to the scope of the function, when you do console.log(i) inside the anonymous function, you're logging the second argument, or index, which in this demo is always zero:

var dataz = ["teal", "brown", "blue", "purple"];

for (var i = 0; i < dataz.length; i++) {
    d3.select("body").attr("fill", (d,i)=>console.log("the value of i is: " + i));
}
<script src="https://d3js.org/d3.v4.min.js"></script>

A simple solution is changing the variable name of the for loop (or the second argument, it doesn't matter). Check this demo, the second argument (i) is always zero, but the loop (j) goes from 0 to 3 (you don't need let here, it works with var):

var dataz = ["teal", "brown", "blue", "purple"];

for (var j = 0; j < dataz.length; j++) {
    d3.select("body").attr("fill", (d,i)=>console.log("second argument:" + i + " - loop:" + j));
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • Gerardo, I am very grateful for the answer you have provided. I also do know loops are not generally necessary when you are working with a D3 script, but as you concede yourself sometimes circumstances will arise when you actually need to use a loop. How might I go about passing variable i to the D3 anonymous function ...? – Noobster Jan 13 '17 at 05:43
  • You mean the variable `i` in the loop? Use another name for it (`j`, `k` or whatever). Or another name for the second argument. – Gerardo Furtado Jan 13 '17 at 05:46
  • I mean the following, where `i` gets incremented by the loop: `d3.select("circle").attr("fill", (d,i)=>console.log(i));` Unfortunately the code always returns '0', meaning that I am not correctly passing variable `i`. – Noobster Jan 13 '17 at 05:50
  • That's exactly what I said: you're paying the index, not the `i` in the for loop. JavaScript has function scope. So, use *another* name for the variable in the for loop. – Gerardo Furtado Jan 13 '17 at 05:52
  • No, you did something wrong. Check my second snippet again, I just edited it. – Gerardo Furtado Jan 13 '17 at 05:56
  • This does not work: `for (j = 0; j < dataz.length; j++) { d3.select("circle").attr("fill", (d,i)=>console.log(j)); }` – Noobster Jan 13 '17 at 06:13
  • It's not incrementing. All it _seems_ do be doing is giving me the last value of j ... j many times :) – Noobster Jan 13 '17 at 06:18
  • Did you see the last snippet? It **is** incrementing. Anyway, I give up. – Gerardo Furtado Jan 13 '17 at 07:42
  • Sorry Gerardo, I am obviously not trying to frustrate you. There is clearly something not going quite right with my code. Thanks for your help. – Noobster Jan 13 '17 at 07:46
  • If it's not incrementing, you probably have a scope problem (`var` versus `let`, [see here](http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example)). Please, post **all** your code. – Gerardo Furtado Jan 13 '17 at 07:47
  • Whilst I may not have resolved my issue I can nonetheless see that your answer is correct. So I must accept it as the answer. – Noobster Jan 13 '17 at 07:49
  • Honestly, I don't mind about accepting/not accepting the answer, that's not the point. I just want to solve the mystery, which is the fact that you said that this: `for (j = 0; j < dataz.length; j++) { d3.select("circle").attr("fill", (d,i)=>console.log(j)); }` was not working, since my snippet clearly shows it working. – Gerardo Furtado Jan 13 '17 at 07:51
  • AS you point out yourself that is most likely due to a scope issue. I will work though my code to identify the issue. Now that I know the correct answer I can work back from the correct assumption. – Noobster Jan 13 '17 at 07:52
  • Check my answer. Problem is not just variable name, but the value of that variable when used from the `fill` attribute data function. Since it is not run synchronously with the loop, the value of `i` (or whatever you call the loop index variable to prevent shadowing the `i` argument to the data function) will be unreliable at call time. – Óscar Gómez Alcañiz Jan 14 '17 at 23:34
  • @ÓscarGómezAlcañiz No, that's not the case. Check my third snippet. – Gerardo Furtado Jan 15 '17 at 01:37
0

Disregarding the name of the variables (that could be the issue on other cases), the problem here is due to the loop index variable not existing or having a different value when accessed from the D3js data function.

The only way to make sure the current loop's value is used inside the data function is to supply it when creating the data function, so this index value exists inside the data function's scope.

This should work (didn't try!):

  for (idx = 0; idx < dataz.length; idx++) {
    d3.select(list[idx])
      .style("fill", dataz[i].color)
      // Immediately Invoked Function Expression will do the trick!
      .style("fill-opacity", (function(d, i) {
        if (dataz[idx].color === "#ffff") {
          return "0";
        } else {
          return "0.5"
        }
      })(idx));
  }
  • *"The only way to make sure the current loop's value is used inside the data function is to supply it when creating the data function"* This is not correct. Just changing the variable name, without any IIFE, works very well: https://jsfiddle.net/gerardofurtado/crxty2s7/ Besides that, there is no data function in OP's question. – Gerardo Furtado Jan 15 '17 at 08:34
  • 1
    You are right, I thought that the function supplied as argument was called at a different time but it's not the case. Regarding the "data function", I call that way to the `function (d, i) { ... }` supplied as value for the `fill-opacity` style on the `style ()` method. Maybe I used the wrong words... What do you understand by "data function"? – Óscar Gómez Alcañiz Jan 15 '17 at 15:34
  • The data function is the function that takes the data array... It's literally named data: `data([array])`.This one here: https://github.com/d3/d3-selection/blob/master/README.md#selection_data – Gerardo Furtado Jan 16 '17 at 03:55