Control flow

each()

Method each() can be used to invoke arbitrary code for each selected element:


const countries = [
  {name: "Germany", population: 84_270_625},
  {name: "France", population: 68_042_591},
  {name: "Spain", population: 47_222_613},
  {name: "the Netherlands", population: 17_882_900},		
  {name: "Sweden", population: 10_481_937},	
];
	
d3.select("body")
  .append("svg")
  .selectAll("rect")
  .data(countries)
  .join("rect")  
  .each((d, i) => console.log(d.name));
/* logs:"
Germany
France
Spain
the Netherlands
Sweden
"*/  

call()

The call() method calls the specified function once on each element in the selection. The selection itself is passed into the specified function as the first argument. Method call() takes optional second and next arguments that are passed into the function as the second and next arguments. Method call() returns the selection again, which facilitates method chaining.

Method call() is typically useful where you want a reusable function to operate on selections.


function setFill(selection, color) {
  selection
    .style('fill', color);
};

setFill(d3.selectAll("circle"), "magenta"); // does not return the selection!

Or:


function setFill(selection, color) {
  selection
    .style('fill', color);
};

d3.selectAll("circle")
  .call(setFill, "magenta")  // returns the selection
  .style("stroke", "green")
  .style("stroke-width", "2px");

Result:

We can also use call() to append multiple elements to a container, as shown in the next example.


const players = [
 { name: "Joe", score: 35},
 { name: "Abby", score: 16},
 { name: "Casey", score: 44},
 { name: "Max", score: 62}
];	

const maxScore = d3.max(players, d => d.score); // 62
const barWidth = 20;
const fontSize = 6;	

const svg = d3.select("svg")
  .attr("font-family", "sans-serif")
  .attr("font-size", fontSize)
  .attr("text-anchor", "middle")
  .attr("viewBox", `0 0 ${players.length * barWidth} ${maxScore + (fontSize * 2)}`);

const bar = svg.selectAll("g")
  .data(players)
  .join("g")
    .attr("transform", (d, i) => `translate(${i * barWidth}, 0)`);	

bar
  .append("rect")
    .style('fill', "orange")
    .attr('width', barWidth - 1)	  
    .attr('height', d => d.score )	  
    .attr('y', fontSize + 2);;  

bar
  .append("text")
    .style('fill', "gray")
    .attr('dx', barWidth/2 - 0.5 )
    .attr('y', fontSize)
    .text(d => d.name );	
Or...

const players = [
 { name: "Joe", score: 35},
 { name: "Abby", score: 16},
 { name: "Casey", score: 44},
 { name: "Max", score: 62}
];	

const maxScore = d3.max(players, d => d.score);
const barWidth = 20;
const fontSize = 6;	

const svg = d3.select("svg")
  .attr("font-family", "sans-serif")
  .attr("font-size", fontSize)
  .attr("text-anchor", "middle")
  .attr("viewBox", `0 0 ${players.length * barWidth} ${maxScore + (fontSize * 2)}`);

// Create the bars and axis:
svg.selectAll("g")
  .data(players)
  .join("g")
    .attr("transform", (d, i) => `translate(${i * barWidth}, 0)`)
    .call(g => g.append("rect")
      .style('fill', "orange")
      .attr('width', barWidth - 1)	  
      .attr('height', d => d.score )
      .attr('y', fontSize + 2)  
    )
    .call(g => g.append("text")
      .style('fill', "gray")
      .attr('dx', barWidth/2 - 0.5 )
      .attr('y', fontSize)		
      .text(d => d.name )	
    );	

Result:

PS. Note that it is generally easier to use a band scale to create a bar chart with a discrete domain. Later more about this.