Sunday, 17 November 2024

Web Tutorial: D3 Head-to-head Line Chart (Part 2/2)

Welcome back! We'll be adding stuff to some of the methods already written, and adding some new properties and methods to the h2h object.

Let's begin by modifying the getData() method. This code, by virtue of its very placement, runs only if team1 is not equal to team2. We use a For loop to iterate through the data array. Since we already know the seasons go from 2018 to 2023, we can cheat a little and use season as an index. Our objective is to to extract an array of matches which feature team1 against team2, regardless of which team is home or away.
getData: function()
{
  var ddlTeam1 = d3.select("#ddlTeam1");
  var ddlTeam2 = d3.select("#ddlTeam2");

  var team1 = ddlTeam1.node().value;
  var team2 = ddlTeam2.node().value;
  this.currentData = [];

  if (team1 == team2) return;

  for (var season = 2018; season <= 2023; season ++)
  {

  }

},


In each iteration, we have another For loop that traverses through the current object.
for (var season = 2018; season <= 2023; season ++)
{
  for (var i = 0; i < this.data[season].length; i++)
  {

  }

}


Declare h and a. h is the name of the home team and a is the name of the away team. Then declare score1 and score2 with default values of 0.
for (var season = 2018; season <= 2023; season ++)
{
  for (var i = 0; i < this.data[season].length; i++)
  {
    var h = this.data[season][i].home;
    var a = this.data[season][i].away;
    var score1 = 0;
    var score2 = 0;

  }
}


We only proceed if team1 is the home team and team2 is the away team, or team2 is the home team and team1 is the away team. Basically, if the current object does not represent a match between team1 and team2, use the continue statement to move on.
for (var season = 2018; season <= 2023; season ++)
{
  for (var i = 0; i < this.data[season].length; i++)
  {
    var h = this.data[season][i].home;
    var a = this.data[season][i].away;
    var score1 = 0;
    var score2 = 0;
  
    if (!((h.team == team1 && a.team == team2) || (h.team == team2 && a.team == team1))) continue;
  }
}


Now, we use If blocks. There are only two possible cases at this point - team1 or team2 is home. If home, we set score1 and score2.
for (var season = 2018; season <= 2023; season ++)
{
  for (var i = 0; i < this.data[season].length; i++)
  {
    var h = this.data[season][i].home;
    var a = this.data[season][i].away;
    var score1 = 0;
    var score2 = 0;
  
    if (!((h.team == team1 && a.team == team2) || (h.team == team2 && a.team == team1))) continue;
  
    if (h.team == team1)
    {
      score1 = h.goals;
      score2 = a.goals;
    }

  }
}


Same if team2 is home.
for (var season = 2018; season <= 2023; season ++)
{
  for (var i = 0; i < this.data[season].length; i++)
  {
    var h = this.data[season][i].home;
    var a = this.data[season][i].away;
    var score1 = 0;
    var score2 = 0;
  
    if (!((h.team == team1 && a.team == team2) || (h.team == team2 && a.team == team1))) continue;
  
    if (h.team == team1)
    {
      score1 = h.goals;
      score2 = a.goals;
    }
  
    if (h.team == team2)
    {
      score1 = a.goals;
      score2 = h.goals;
    }  

  }
}


Then we'll push the data into currentData as an object.
for (var season = 2018; season <= 2023; season ++)
{
  for (var i = 0; i < this.data[season].length; i++)
  {
    var h = this.data[season][i].home;
    var a = this.data[season][i].away;
    var score1 = 0;
    var score2 = 0;
  
    if (!((h.team == team1 && a.team == team2) || (h.team == team2 && a.team == team1))) continue;
  
    if (h.team == team1)
    {
      score1 = h.goals;
      score2 = a.goals;
    }
  
    if (h.team == team2)
    {
      score1 = a.goals;
      score2 = h.goals;
    }
  
    this.currentData.push({ "season": season, "team1": team1, "team2": team2, "score1": score1, "score2": score2});
  }
}


Now let's go to the method renderLineChart(). In here, we add a call to the methods renderAxis_x(), renderAxis_y() and renderPlots(). This means that these methods will only get called if there's data in the currentData array, because if there was no data, the renderLineChart() method would end before getting to these method calls.
renderLineChart: function()
{
  var chart = d3.selectAll("#chart");
  chart.html("");

  if (this.currentData.length == 0)
  {
    chart
    .append("text")
    .attr("class", "nodata")
    .attr("x", "400px")
    .attr("y", "200px")
    .text("No data. Please select another pair of teams.");
    return;
  }

  this.renderAxis_x();
  this.renderAxis_y();
  this.renderPlots();

}


Add these three methods.
  renderLineChart: function()
  {
    var chart = d3.selectAll("#chart");
    chart.html("");
  
    if (this.currentData.length == 0)
    {
      chart
      .append("text")
      .attr("class", "nodata")
      .attr("x", "400px")
      .attr("y", "200px")
      .text("No data. Please select another pair of teams.");
      return;
    }
  
    this.renderAxis_x();
    this.renderAxis_y();
    this.renderPlots();
  },
  renderAxis_x: function()
  {

  },
  renderAxis_y: function()
  {

  },
  renderPlots: function()
  {

  }

};


Time to work on the x-axis. We begin by declaring chart, and then using the selectAll() method of the d3 object to set it to the SVG with the id of chart.
renderAxis_x: function()
{
  var chart = d3.selectAll("#chart");
},


Here, we add a horizontal line styled using the axis_x CSS class.
renderAxis_x: function()
{
  var chart = d3.selectAll("#chart");

  chart
  .append("line")
  .attr("class", "axis_x")

},


Then we specify the x1, x2, y1 and y2 attributes of the line. Note that we use the margin property of h2h so that we can observe a buffer space within the SVG chart.
renderAxis_x: function()
{
  var chart = d3.selectAll("#chart");

  chart
  .append("line")
  .attr("class", "axis_x")
  .attr("x1", h2h.margin + "px")
  .attr("y1", (400 - h2h.margin) + "px")
  .attr("x2", (800 - h2h.margin) + "px")
  .attr("y2", (400 - h2h.margin) + "px");

},


We specify margin here. It's neater to just place properties together. I've set 40 as the number of pixels that the buffer takes up all round, but feel free to try something else.
currentData: [],
margin: 40,
getData: function()
{


Here's the styling for axis_x. It's just a grey thin line. To save time, let's have axis_y, x_tick and y_tick all styled the same way too.
#chartContainer, #chart
{
  width: 800px;
  height: 400px;
  margin: 0 auto 0 auto;
}

.axis_x, .x_tick, .axis_y, .y_tick
{
  stroke: rgba(50, 50, 50, 1);
  stroke-width: 1px;
}


.nodata
{
  font: bold 30px arial;
  fill: rgba(0, 0, 0, 0.5);
  text-anchor: middle;
}


See this line?


We will be adding ticks! In renderAxis_x(), declare hspacing and set it to the value returned by calling the getHorizontalSpacing() method. Pass in the length of currentData as an argument.
chart
.append("line")
.attr("class", "axis_x")
.attr("x1", h2h.margin + "px")
.attr("y1", (400 - h2h.margin) + "px")
.attr("x2", (800 - h2h.margin) + "px")
.attr("y2", (400 - h2h.margin) + "px");

var hspacing = this.getHorizontalSpacing(h2h.currentData.length);


Add getHorizontalSpacing(). This method is supposed to calculate how much spacing there should be between each season's data point, assuming there is limit to the horizontal space available and we need to divide that equally between the data points. To that end, we will return 800, less margin two times, and the remainder will be divided by the value of the parameter maxSeasons. And because it's better to return a whole number, we'll use the Math object's floor() method on the result.
getData: function()
{
  var ddlTeam1 = d3.select("#ddlTeam1");
  var ddlTeam2 = d3.select("#ddlTeam2");

  var team1 = ddlTeam1.node().value;
  var team2 = ddlTeam2.node().value;
  this.currentData = [];

  if (team1 == team2) return;

  for (var season = 2018; season <= 2023; season ++)
  {
    for (var i = 0; i < this.data[season].length; i++)
    {
      var h = this.data[season][i].home;
      var a = this.data[season][i].away;
      var score1 = 0;
      var score2 = 0;

      if (!((h.team == team1 && a.team == team2) || (h.team == team2 && a.team == team1))) continue;

      if (h.team == team1)
      {
        score1 = h.goals;
        score2 = a.goals;
      }

      if (h.team == team2)
      {
        score1 = a.goals;
        score2 = h.goals;
      }

      this.currentData.push({ "season": season, "team1": team1, "team2": team2, "score1": score1, "score2": score2});
    }
  }
},
getHorizontalSpacing: function(maxSeasons)
{
  return Math.floor((800 - h2h.margin - h2h.margin) / maxSeasons);
},

changeTeams: function()
{
  var imgTeam1 = d3.select("#team1Logo");
  var ddlTeam1 = d3.select("#ddlTeam1");

  var imgTeam2 = d3.select("#team2Logo");
  var ddlTeam2 = d3.select("#ddlTeam2");

  for (var i = 0; i < this.teams.length; i++)
  {
    if (ddlTeam1.node().value == this.teams[i].name) imgTeam1.attr("style", "background-image: url(" + this.teams[i].logo + ")");
    if (ddlTeam2.node().value == this.teams[i].name)imgTeam2.attr("style", "background-image: url(" + this.teams[i].logo + ")");
  }

  this.getData();

  this.renderLineChart();
},


Now that we have hspacing, let's get back to the renderAxis_x() method. We want to append line tags to chart, styled using the x_tick CSS class. We'll specify currentData as data.
chart
.append("line")
.attr("class", "axis_x")
.attr("x1", h2h.margin + "px")
.attr("y1", (400 - h2h.margin) + "px")
.attr("x2", (800 - h2h.margin) + "px")
.attr("y2", (400 - h2h.margin) + "px");

var hspacing = this.getHorizontalSpacing(h2h.currentData.length);

chart.selectAll("line.x_tick")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "x_tick")


We then specify x1 and x2. They have the same value, because the ticks along the x-axis are basically short vertical lines. For both of them, we make sure that there's a buffer at the beginning by adding margin, then add hspacing multiplied by the current index, i. This means, progressively, each data point's x1 and x2 properties will move further from the left.
chart.selectAll("line.x_tick")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "x_tick")
.attr("x1", function(d, i)
{
  return (h2h.margin + (i * hspacing)) + "px";
})
.attr("x2", function(d, i)
{
  return (h2h.margin + (i * hspacing)) + "px";
})


Then we have y1. We take the maximum height, 400, less margin, as the starting point.
chart.selectAll("line.x_tick")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "x_tick")
.attr("style", function(d, i)
{
  return (i == 0 || i % 2 == 0 ? "" : "");
})
.attr("x1", function(d, i)
{
  return (h2h.margin + (i * hspacing)) + "px";
})
.attr("y1", (400 - h2h.margin) + "px")
.attr("x2", function(d, i)
{
  return (h2h.margin + (i * hspacing)) + "px";
})


For y2, we do the same with y1, except that we subtract one quarter of margin from the result.
chart.selectAll("line.x_tick")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "x_tick")
.attr("style", function(d, i)
{
  return (i == 0 || i % 2 == 0 ? "" : "");
})
.attr("x1", function(d, i)
{
  return (h2h.margin + (i * hspacing)) + "px";
})
.attr("y1", (400 - h2h.margin) + "px")
.attr("x2", function(d, i)
{
  return (h2h.margin + (i * hspacing)) + "px";
})
.attr("y2", (400 - (h2h.margin - (h2h.margin / 4))) + "px");


See the ticks?


Now we're going to add labels. In chart, we add text tags styled using the CSS class x_label.
chart.selectAll("line.x_tick")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "x_tick")
.attr("x1", function(d, i)
{
  return (h2h.margin + (i * hspacing)) + "px";
})
.attr("y1", (400 - h2h.margin) + "px")
.attr("x2", function(d, i)
{
  return (h2h.margin + (i * hspacing)) + "px";
})
.attr("y2", (400 - (h2h.margin - (h2h.margin / 4))) + "px");

chart.selectAll("text.x_label")
.data(h2h.currentData)
.enter()
.append("text")
.attr("class", "x_label")  


The x attribute will utilize a formula, the same that we used for the ticks.
chart.selectAll("text.x_label")
.data(h2h.currentData)
.enter()
.append("text")
.attr("class", "x_label")
.attr("x", function(d, i)
{
  return (h2h.margin + (i * hspacing)) + "px";
})


The y attribute is just a formula that will anchor the text tag just below the x-axis line.
chart.selectAll("text.x_label")
.data(h2h.currentData)
.enter()
.append("text")
.attr("class", "x_label")
.attr("x", function(d, i)
{
  return (h2h.margin + (i * Math.floor((800 - h2h.margin - h2h.margin) / h2h.currentData.length ))) + "px";
})
.attr("y", (400 - (h2h.margin - (h2h.margin / 2))) + "px")


Finally, the text attribute will be the season property of the current element. However, since there are two matches per season, so we only want the season label to appear for one of them. Thus, we have a conditional block to test if the current index, i, is 0 or an even number (using the Modulus operator). If this is true, the label is blank.
chart.selectAll("text.x_label")
.data(h2h.currentData)
.enter()
.append("text")
.attr("class", "x_label")
.attr("x", function(d, i)
{
  return (h2h.margin + (i * Math.floor((800 - h2h.margin - h2h.margin) / h2h.currentData.length ))) + "px";
})
.attr("y", (400 - (h2h.margin - (h2h.margin / 2))) + "px")
.text(function(d, i)
{
  return (i == 0 || i % 2 == 0 ? "" : );
});


If not, we use the current element's season parameter, and add the following year to it.
chart.selectAll("text.x_label")
.data(h2h.currentData)
.enter()
.append("text")
.attr("class", "x_label")
.attr("x", function(d, i)
{
  return (h2h.margin + (i * Math.floor((800 - h2h.margin - h2h.margin) / h2h.currentData.length ))) + "px";
})
.attr("y", (400 - (h2h.margin - (h2h.margin / 2))) + "px")
.text(function(d, i)
{
  return (i == 0 || i % 2 == 0 ? "" : d.season + " / " + (d.season + 1));
});


Here, we style x_label. To save time, let's define this as the styling for y_label as well. We want to specify the font, color and make sure that the text-anchor property is set to middle.
.axis_x, .x_tick, .axis_y, .y_tick
{
  stroke: rgba(50, 50, 50, 1);
  stroke-width: 1px;
}

.x_label, .y_label
{
  font: bold 10px arial;
  fill: rgba(0, 0, 0, 0.5);
  text-anchor: middle;
}


.nodata
{
  font: bold 30px arial;
  fill: rgba(0, 0, 0, 0.5);
  text-anchor: middle;
}


This is how the labels will appear.


Now for the y-axis! A lot of the actions we take here will be analogous to what we did for the x-axis. Add code to the renderAxis_y() method. Here' we're adding the line to chart and setting the styling to the CSS class axis_y. The rest of the properties - x1, x2, y1 and y2 are hopefully self-explanatory by now.
renderAxis_y: function()
{
  var chart = d3.selectAll("#chart");

  chart
  .append("line")
  .attr("class", "axis_y")
  .attr("x1", h2h.margin + "px")
  .attr("y1", h2h.margin + "px")
  .attr("x2", h2h.margin + "px")
  .attr("y2", (400 - h2h.margin) + "px");

},


Here's the line!


We then define max as the highest number of goals scored in any match within the current dataset, currentData. For this, we use d3's max() method and use a conditional block to compare score1 and score2, returning the higher value.
renderAxis_y: function()
{
  var chart = d3.selectAll("#chart");

  chart
  .append("line")
  .attr("class", "axis_y")
  .attr("x1", h2h.margin + "px")
  .attr("y1", h2h.margin + "px")
  .attr("x2", h2h.margin + "px")
  .attr("y2", (400 - h2h.margin) + "px");

  var max = d3.max(h2h.currentData, function(d) { return (d.score1 > d.score2 ? d.score1 : d.score2);});
}


Now define vspacing as the value returned by running the getVerticalSpacing() method with max passed in as an argument.
renderAxis_y: function()
{
  var chart = d3.selectAll("#chart");

  chart
  .append("line")
  .attr("class", "axis_y")
  .attr("x1", h2h.margin + "px")
  .attr("y1", h2h.margin + "px")
  .attr("x2", h2h.margin + "px")
  .attr("y2", (400 - h2h.margin) + "px");

  var max = d3.max(h2h.currentData, function(d) { return (d.score1 > d.score2 ? d.score1 : d.score2);});
  var vspacing = this.getVerticalSpacing(max);
}


We then define getVerticalSpacing() as a method. Instead of using the number of seasons like we did for getHorizontalSpacing(), we define the vertical space available for each data point from the maximum number of goals.
getHorizontalSpacing: function(maxSeasons)
{
  return Math.floor((800 - h2h.margin - h2h.margin) / maxSeasons);
},
getVerticalSpacing: function(maxGoals)
{
  return Math.floor((400 - h2h.margin - h2h.margin) / maxGoals);
},

changeTeams: function()
{


We want an array of goal values, from 1 to max. To do that, we define goalsArr as an empty array, then using a For loop to iterate from 1 to max, we push each value to goalsArr.
var max = d3.max(h2h.currentData, function(d) { return (d.score1 > d.score2 ? d.score1 : d.score2);});
var vspacing = this.getVerticalSpacing(max);

var goalsArr = [];
for (var i = 1; i <= max; i++)
{
  goalsArr.push(i);
}  


Now we start adding ticks to the y-axis. These are line tags, appended to chart, styled using the y_tick CSS class. And we use goalsArr as the data source.
var max = d3.max(h2h.currentData, function(d) { return (d.score1 > d.score2 ? d.score1 : d.score2);});
var vspacing = this.getVerticalSpacing(max);

var goalsArr = [];
for (var i = 1; i <= max; i++)
{
  goalsArr.push(i);
}      

chart.selectAll("line.y_tick")
.data(goalsArr)
.enter()
.append("line")
.attr("class", "y_tick")


Since it's a horizontal line, x1 and x2 should be different. x1 will always be the same value as margin, which means the line starts horizontally at margin. And x2 will be margin less a quarter of margin.
chart.selectAll("line.y_tick")
.data(goalsArr)
.enter()
.append("line")
.attr("class", "y_tick")
.attr("x1", h2h.margin + "px")
.attr("x2", (h2h.margin - (h2h.margin / 4)) + "px")


y1 and y2 will be the same. We want a buffer space of margin, and use the index, i, to multiply by vspacing, and add the result to margin. In other words, we do what we did for the x-axis, except vertically.
chart.selectAll("line.y_tick")
.data(goalsArr)
.enter()
.append("line")
.attr("class", "y_tick")
.attr("x1", h2h.margin + "px")
.attr("y1", function(d)
{
  return ((400 - h2h.margin) - (d * vspacing)) + "px";
})

.attr("x2", (h2h.margin - (h2h.margin / 4)) + "px")
.attr("y2", function(d)
{
  return ((400 - h2h.margin) - (d * vspacing)) + "px";
});


The ticks.

We're adding labels next. So it's text tags appended to chart, styled using the CSS class y_label. Again, the dataset is goalsArr.
  chart.selectAll("line.y_tick")
  .data(goalsArr)
  .enter()
  .append("line")
  .attr("class", "y_tick")
  .attr("x1", h2h.margin + "px")
  .attr("y1", function(d)
  {
    return ((400 - h2h.margin) - (d * vspacing)) + "px";
  })
  .attr("x2", (h2h.margin - (h2h.margin / 4)) + "px")
  .attr("y2", function(d)
  {
    return ((400 - h2h.margin) - (d * vspacing)) + "px";
  });

  chart.selectAll("text.y_label")
  .data(goalsArr)
  .enter()
  .append("text")
  .attr("class", "y_label")  
  
},


We use the text() method to make goalsArr's current element's value, the text value.
chart.selectAll("text.y_label")
.data(goalsArr)
.enter()
.append("text")
.attr("class", "y_label")
.text(function(d)
{
  return d;
});  


The x attribute will be a function of margin. I'm going to make it half of margin, less 10 pixels.
chart.selectAll("text.y_label")
.data(goalsArr)
.enter()
.append("text")
.attr("class", "y_label")
.attr("x", ((h2h.margin / 2) - 10) + "px")
.text(function(d)
{
  return d;
});


The y attribute will depend on the current element. We have the maximum height, 400, less the margin. Then we have the value d multiplied by vspacing, subtracted from the result. After that, we add 5 pixels to align the label.
chart.selectAll("text.y_label")
.data(goalsArr)
.enter()
.append("text")
.attr("class", "y_label")
.attr("x", ((h2h.margin / 2) - 10) + "px")
.attr("y", function(d)
{
  return ((400 - h2h.margin) - (d * vspacing) + 5) + "px";
})

.text(function(d)
{
  return d;
});


You can see that the maximum number of goals scored by one team in matches between Manchester City and Arsenal is 5. This is reflected in the y-axis.


Finally, the plots!

We move on to the renderPlots() method. It starts similar to renderAxis_y(), defining chart and max the same way. For good measure, we'll also need hspacing and vspacing, utilizing the getHorizontalSpacing() and getVerticalSpacing() methods respectively.
renderPlots: function()
{
  var chart = d3.selectAll("#chart");

  var max = d3.max(h2h.currentData, function(d) { return (d.score1 > d.score2 ? d.score1 : d.score2);});
  var vspacing = this.getVerticalSpacing(max);
  var hspacing = this.getHorizontalSpacing(h2h.currentData.length);

}


Declare color1. That's the color that the first team's (from the first drop-down list) data will be rendered in. Thus, we want to get the value from that drop-down list and reference teams, then get the color property value of the appropriate element. We're going to use the filter() method on the teams array.
renderPlots: function()
{
  var chart = d3.selectAll("#chart");

  var max = d3.max(h2h.currentData, function(d) { return (d.score1 > d.score2 ? d.score1 : d.score2);});
  var vspacing = this.getVerticalSpacing(max);
  var hspacing = this.getHorizontalSpacing(h2h.currentData.length);

  var color1 = h2h.teams.filter((x) => {} );
}


We match team's current element's name property against ddlTeam1's selected value.
renderPlots: function()
{
  var chart = d3.selectAll("#chart");

  var max = d3.max(h2h.currentData, function(d) { return (d.score1 > d.score2 ? d.score1 : d.score2);});
  var vspacing = this.getVerticalSpacing(max);
  var hspacing = this.getHorizontalSpacing(h2h.currentData.length);

  var color1 = h2h.teams.filter((x) => {return x.name == d3.select("#ddlTeam1").node().value;} );
}


There will be an array with only one element, so reference that and get the color property.
renderPlots: function()
{
  var chart = d3.selectAll("#chart");

  var max = d3.max(h2h.currentData, function(d) { return (d.score1 > d.score2 ? d.score1 : d.score2);});
  var vspacing = this.getVerticalSpacing(max);
  var hspacing = this.getHorizontalSpacing(h2h.currentData.length);

  var color1 = h2h.teams.filter((x) => {return x.name == d3.select("#ddlTeam1").node().value;} )[0].color;
}


Do the same for color2, only we search ddlTeam2's selected value instead.
renderPlots: function()
{
  var chart = d3.selectAll("#chart");

  var max = d3.max(h2h.currentData, function(d) { return (d.score1 > d.score2 ? d.score1 : d.score2);});
  var vspacing = this.getVerticalSpacing(max);
  var hspacing = this.getHorizontalSpacing(h2h.currentData.length);

  var color1 = h2h.teams.filter((x) => {return x.name == d3.select("#ddlTeam1").node().value;} )[0].color;
  var color2 = h2h.teams.filter((x) => {return x.name == d3.select("#ddlTeam2").node().value;} )[0].color;
}


Here, we append circle tags to chart. The dataset is currentData, and the CSS class is node1.
var color2 = h2h.teams.filter((x) => {return x.name == d3.select("#ddlTeam2").node().value;} )[0].color;

chart.selectAll("circle.node1")
.data(h2h.currentData)
.enter()
.append("circle")
.attr("class", "node1")  


We esure that the circle's fill attribute is color1, and that it has a 5 pixel radius.
chart.selectAll("circle.node1")
.data(h2h.currentData)
.enter()
.append("circle")
.attr("class", "node1")
.attr("fill", color1)  
.attr("r", "5px")


The cx attribute will use almost the same formula for the x1 and x2 properties of the ticks on the x-axis.
chart.selectAll("circle.node1")
.data(h2h.currentData)
.enter()
.append("circle")
.attr("class", "node1")
.attr("fill", color1)  
.attr("r", "5px")
.attr("cx", function(d, i)
{
  return (h2h.margin + (i * hspacing)) + "px";
})


Except that we want to move them half a hspacing to the right.
chart.selectAll("circle.node1")
.data(h2h.currentData)
.enter()
.append("circle")
.attr("class", "node1")
.attr("fill", color1)  
.attr("r", "5px")
.attr("cx", function(d, i)
{
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})


And the cy property will account for a buffer the size of margin, then use the current element's score1 value to multiply by vspacing, to determine the circle's vertical positioning.
chart.selectAll("circle.node1")
.data(h2h.currentData)
.enter()
.append("circle")
.attr("class", "node1")
.attr("fill", color1)  
.attr("r", "5px")
.attr("cx", function(d, i)
{
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("cy", function(d)
{
  return ((400 - h2h.margin) - (d.score1 * vspacing)) + "px";
});


You'll see the pale blue circles.


For this next step, just copy and paste what we did.
chart.selectAll("circle.node1")
.data(h2h.currentData)
.enter()
.append("circle")
.attr("class", "node1")
.attr("fill", color1)  
.attr("r", "5px")
.attr("cx", function(d, i)
{
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("cy", function(d)
{
  return ((400 - h2h.margin) - (d.score1 * vspacing)) + "px";
});

chart.selectAll("circle.node1")
.data(h2h.currentData)
.enter()
.append("circle")
.attr("class", "node1")
.attr("fill", color1)  
.attr("r", "5px")
.attr("cx", function(d, i)
{
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("cy", function(d)
{
  return ((400 - h2h.margin) - (d.score1 * vspacing)) + "px";
});  


And then make the necessary changes to display the goals for the second team.
chart.selectAll("circle.node1")
.data(h2h.currentData)
.enter()
.append("circle")
.attr("class", "node1")
.attr("fill", color1)  
.attr("r", "5px")
.attr("cx", function(d, i)
{
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("cy", function(d)
{
  return ((400 - h2h.margin) - (d.score1 * vspacing)) + "px";
});

chart.selectAll("circle.node2")
.data(h2h.currentData)
.enter()
.append("circle")
.attr("class", "node2")
.attr("fill", color2)  
.attr("r", "5px")
.attr("cx", function(d, i)
{
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("cy", function(d)
{
  return ((400 - h2h.margin) - (d.score2 * vspacing)) + "px";
});


Now you can see the goals scored for Arsenal, rendered in scarlet.

Time to join the dots! We'll start with the first team's data. Append line tags to chart, and style them using the CSS class line1. As before, the dataset used is currentData. Make sure the stroke attribute is set to color1.
chart.selectAll("circle.node2")
.data(h2h.currentData)
.enter()
.append("circle")
.attr("class", "node2")
.attr("fill", color2)  
.attr("r", "5px")
.attr("cx", function(d, i)
{
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("cy", function(d)
{
  return ((400 - h2h.margin) - (d.score2 * vspacing)) + "px";
});  

chart.selectAll("line.line1")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "line1")
.attr("stroke", color1)  


The index value, i, is going to be important when we determine x1, x2, y1 and y2. If it's the very first element of currentData, we will start at the left end, so we return "0px" automatically. Otherwise, we'll follow what we did for the circle tags' cx attributes.
chart.selectAll("line.line1")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "line1")
.attr("stroke", color1)  
.attr("x1", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})


For x2, we do the same except that since our destination is the previous node, we reference i minus 1 instead of just i. No need to worry about an invalid index because of the first If block.
chart.selectAll("line.line1")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "line1")
.attr("stroke", color1)  
.attr("x1", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("x2", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + ((i - 1) * hspacing) + (hspacing / 2)) + "px";
})


For y1, again we return 0px if it's the first element in currentData. If not, we do what we did for the circle tags' cy attributes.
chart.selectAll("line.line1")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "line1")
.attr("stroke", color1)  
.attr("x1", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("y1", function(d, i)
{
  if (i == 0) return "0px";
  return ((400 - h2h.margin) - (d.score1 * vspacing)) + "px";
})

.attr("x2", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + ((i - 1) * hspacing) + (hspacing / 2)) + "px";
})


y2 is the same, except that we reference i minus 1 rather than i.
chart.selectAll("line.line1")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "line1")
.attr("stroke", color1)  
.attr("x1", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("y1", function(d, i)
{
  if (i == 0) return "0px";
  return ((400 - h2h.margin) - (d.score1 * vspacing)) + "px";
})
.attr("x2", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + ((i - 1) * hspacing) + (hspacing / 2)) + "px";
})
.attr("y2", function(d, i)
{
  if (i == 0) return "0px";
  return ((400 - h2h.margin) - (h2h.currentData[i - 1].score1 * vspacing)) + "px";
});


In the CSS, we give line1 a 3 pixel stroke width. Do the same for line2.
.nodata
{
  font: bold 30px arial;
  fill: rgba(0, 0, 0, 0.5);
  text-anchor: middle;
}

.line1, .line2
{
  stroke-width: 3px;
}


You see the lovely pale blue lines of Manchester City's goal data?


Now do the same, but for the second team's data.
chart.selectAll("line.line1")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "line1")
.attr("stroke", color1)  
.attr("x1", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("y1", function(d, i)
{
  if (i == 0) return "0px";
  return ((400 - h2h.margin) - (d.score1 * vspacing)) + "px";
})
.attr("x2", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + ((i - 1) * hspacing) + (hspacing / 2)) + "px";
})
.attr("y2", function(d, i)
{
  if (i == 0) return "0px";
  return ((400 - h2h.margin) - (h2h.currentData[i - 1].score1 * vspacing)) + "px";
});

chart.selectAll("line.line2")
.data(h2h.currentData)
.enter()
.append("line")
.attr("class", "line2")
.attr("stroke", color2)  
.attr("x1", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + (i * hspacing) + (hspacing / 2)) + "px";
})
.attr("y1", function(d, i)
{
  if (i == 0) return "0px";
  return ((400 - h2h.margin) - (d.score2 * vspacing)) + "px";
})
.attr("x2", function(d, i)
{
  if (i == 0) return "0px";
  return (h2h.margin + ((i - 1) * hspacing) + (hspacing / 2)) + "px";
})
.attr("y2", function(d, i)
{
  if (i == 0) return "0px";
  return ((400 - h2h.margin) - (h2h.currentData[i - 1].score2 * vspacing)) + "px";
});  


From here, you can see that Arsenal lost by big margins until the 2023/2024 season, where they proceeded to draw with and even beat Manchester City. Quite the turnaround, hey?


Change Arsenal to Liverpool in the second drop-down list, and you'll see this change occur.


Some clean-up

Disable this.
div { outline: 0px solid red; }


And there's your chart, all cleaned up and ready to go.



All done!

I guess compiling and visualizing the data helped me realize just how remarkable Arsenal's most recent performances are, relative to how they were the past five to seven years, against Machester City and Liverpool in particular. This doesn't explain Liverpool's collapse in the 2023/2024 season, but that's another mystery for another day.

All that glitters is not goals!
T___T

Thursday, 14 November 2024

Web Tutorial: D3 Head-to-head Line Chart (Part 1/2)

As a Liverpool FC fan, the 2023/2024 season that ended back in May, was a pretty disappointing one. We started out in such promising fashion, only to stumble at several junctures, and effectively hand the league over to Arsenal and Manchester City.

However. in the midst of puzzling out how the top three teams - Manchester City, Arsenal and Liverpool - have performed against each other since the 2017/2018 season, I had the opportunity to put together a dataset, and then to visualize it using D3. This would ultimately lead to some astonishing insights.

Here we go with some beginning HTML!

You'll notice that we pre-emptively style all divs to have a red border, for temporary structural visibility. We also add a remote link to the D3 library. In the body tag, we add another script tag which we will populate later.
<!DOCTYPE html>
<html>
  <head>
    <title>Head-to-head Line Chart</title>

    <style>
      div { outline: 1px solid red; }
    </style>

    <script src="https://d3js.org/d3.v4.min.js"></script>
  </head>

  <body>
    <script>

    </script>
  </body>
</html>


Then we add a div, id teamsContainer, to the body, and inside it we add two divs. They will use the CSS styles left and right respectively. Right next to teamsContainer, we have another div, id chartContainer. Inside it, we have an SVG with an id of chart.
<!DOCTYPE html>
<html>
  <head>
    <title>Head-to-head Line Chart</title>

    <style>
      div { outline: 1px solid red; }
    </style>

    <script src="https://d3js.org/d3.v4.min.js"></script>
  </head>

  <body>
    <div id="teamsContainer">
      <div class="left">

      </div>
      <div class="right">

      </div>
    </div>


    <div id="chartContainer">
      <svg id="chart">

      </svg>        
    </div>


    <script>

    </script>
  </body>
</html>


Before we continue, let's style some stuff. teamsContainer will have a defined width and height, and we will use the margin property to center it. left and right take up roughly half of teamsContainer in width, and float left and right respectively.
<style>
div { outline: 1px solid red; }

#teamsContainer
{
  width: 800px;
  height: 200px;
  margin: 0 auto 0 auto;
}

.left
{
  width: 49%;
  float: left;
}

.right
{
  width: 49%;
  float: right;
}

</style>


Both chartContainer and chart will have the same defined height, width, and will be centered via the margin property.
<style>
div { outline: 1px solid red; }

#teamsContainer
{
  width: 800px;
  height: 200px;
  margin: 0 auto 0 auto;
}

.left
{
  width: 49%;
  float: left;
}

.right
{
  width: 49%;
  float: right;
}

#chartContainer, #chart
{
  width: 800px;
  height: 400px;
  margin: 0 auto 0 auto;
}

</style>


And then let's add drop-down lists! The CSS class will be ddlTeam.
<div class="left">
  <select class="ddlTeam">
  
  </select>

</div>
<div class="right">
  <select class="ddlTeam">
  
  </select>

</div>


In the CSS, we style ddlTeam accordingly. It's all aesthetics; feel free to go crazy.
.right
{
  width: 49%;
  float: right;
}

.ddlTeam
{
  width: 80%;
  height: 20%;
  text-align: center;
  margin: 0 auto 0 auto;
  border-radius: 3px;
  padding: 2px;
  display: block;
}


#chartContainer, #chart
{
  width: 800px;
  height: 400px;
  margin: 0 auto 0 auto;
}


Just taking shape here.


Now, let's do a bit of scripting! We need a h2h object in the script tag.
<script>
  let h2h =
  {

  };

</script>


In there, add the three properties - teams, data and currentData. teams will be an array, while data is an object. currentData is an empty array, and will stay empty by default until we need to populate it..
<script>
  let h2h =
  {
    teams:
    [
    
    ],
    data:
    {
    
    },
    currentData: []
  };
</script>


As it turns out, teams is an array of objects. The first object has name, color and logo properties. It's for Arsenal, so set color to a translucent scarlet. You'll notice that we have the filename for Arsenal's club crest in logo.
teams:
[
  { "name": "Arsenal", "color": "rgba(255, 0, 0, 0.5)", "logo": "arsenal.png"},
],


And here are the other teams! I have a deep red for Liverpool and a pale blue for Manchester City.
teams:
[
  { "name": "Arsenal", "color": "rgba(255, 0, 0, 0.5)", "logo": "arsenal.png"},
  { "name": "Manchester City", "color": "rgba(100, 200, 255, 0.5)", "logo": "manchestercity.png"},
  { "name": "Liverpool", "color": "rgba(150, 0, 0, 0.5)", "logo": "liverpool.png"}

],


These are the logos I use. I just save them in the same place, out of sheer laziness.

arsenal.png


liverpool.png


manchestercity.png


Now for data. We start off with the property 2018. It's an array.
data:
{
  2018:
  [

  ]

},


To be premise, it's an array of objects. Each object has a home and away property, and they are in turn, objects.
data:
{
  2018:
  [
    {
      "home": { },
      "away": { }
    },

  ]
},


home and away each have team and goals properties. In the first half of the 2018/2019 season, Liverpool hosted Arsenal at home. Thus, home's team property is "Liverpool" and away's team property is "Arsenal". The score was 4 - 0 to Liverpool, and we fill in the goals properties accordingly.
data:
{
  2018:
  [
    {
      "home": { "team": "Liverpool", "goals" : 4},
      "away": { "team": "Arsenal", "goals" : 0}
    },
  ]
},


And then we fill in the second match between Arsenal and Liverpool - the return fixture ended in a 3 - 3 draw.
data:
{
  2018:
  [
    {
      "home": { "team": "Liverpool", "goals" : 4},
      "away": { "team": "Arsenal", "goals" : 0}
    },
    {
      "home": { "team": "Arsenal", "goals" : 3},
      "away": { "team": "Liverpool", "goals" : 3}
    },

  ]
},


And we will fill in the rest of the fixtures between Manchester City and Liverpool, and Manchester City and Arsenal.
2018:
[
  {
    "home": { "team": "Liverpool", "goals" : 4},
    "away": { "team": "Arsenal", "goals" : 0}
  },
  {
    "home": { "team": "Arsenal", "goals" : 3},
    "away": { "team": "Liverpool", "goals" : 3}
  },
  {
    "home": { "team": "Manchester City", "goals" : 3},
    "away": { "team": "Arsenal", "goals" : 1}
  },
  {
    "home": { "team": "Arsenal", "goals" : 0},
    "away": { "team": "Manchester City", "goals" : 3}
  },
  {
    "home": { "team": "Manchester City", "goals" : 5},
    "away": { "team": "Liverpool", "goals" : 0}
  },
  {
    "home": { "team": "Liverpool", "goals" : 4},
    "away": { "team": "Manchester City", "goals" : 3}
  }       
   
]


... and the rest of the seasonal data.
data:
{
  2018:
  [
    {
      "home": { "team": "Liverpool", "goals" : 4},
      "away": { "team": "Arsenal", "goals" : 0}
    },
    {
      "home": { "team": "Arsenal", "goals" : 3},
      "away": { "team": "Liverpool", "goals" : 3}
    },
    {
      "home": { "team": "Manchester City", "goals" : 3},
      "away": { "team": "Arsenal", "goals" : 1}
    },
    {
      "home": { "team": "Arsenal", "goals" : 0},
      "away": { "team": "Manchester City", "goals" : 3}
    },
    {
      "home": { "team": "Manchester City", "goals" : 5},
      "away": { "team": "Liverpool", "goals" : 0}
    },
    {
      "home": { "team": "Liverpool", "goals" : 4},
      "away": { "team": "Manchester City", "goals" : 3}
    }          
  ],
  2019:
  [
    {
      "home": { "team": "Liverpool", "goals" : 5},
      "away": { "team": "Arsenal", "goals" : 1}
    },
    {
      "home": { "team": "Arsenal", "goals" : 1},
      "away": { "team": "Liverpool", "goals" : 1}
    },
    {
      "home": { "team": "Manchester City", "goals" : 3},
      "away": { "team": "Arsenal", "goals" : 1}
    },
    {
      "home": { "team": "Arsenal", "goals" : 0},
      "away": { "team": "Manchester City", "goals" : 2}
    },
    {
      "home": { "team": "Manchester City", "goals" : 2},
      "away": { "team": "Liverpool", "goals" : 1}
    },
    {
      "home": { "team": "Liverpool", "goals" : 0},
      "away": { "team": "Manchester City", "goals" : 0}
    }          
  ],
  2020:
  [
    {
      "home": { "team": "Liverpool", "goals" : 3},
      "away": { "team": "Arsenal", "goals" : 1}
    },
    {
      "home": { "team": "Arsenal", "goals" : 2},
      "away": { "team": "Liverpool", "goals" : 1}
    },
    {
      "home": { "team": "Manchester City", "goals" : 3},
      "away": { "team": "Arsenal", "goals" : 0}
    },
    {
      "home": { "team": "Arsenal", "goals" : 0},
      "away": { "team": "Manchester City", "goals" : 3}
    },
    {
      "home": { "team": "Manchester City", "goals" : 4},
      "away": { "team": "Liverpool", "goals" : 0}
    },
    {
      "home": { "team": "Liverpool", "goals" : 1},
      "away": { "team": "Manchester City", "goals" : 3}
    }          
  ],
  2021:
  [
    {
      "home": { "team": "Liverpool", "goals" : 3},
      "away": { "team": "Arsenal", "goals" : 1}
    },
    {
      "home": { "team": "Arsenal", "goals" : 0},
      "away": { "team": "Liverpool", "goals" : 3}
    },
    {
      "home": { "team": "Manchester City", "goals" : 1},
      "away": { "team": "Arsenal", "goals" : 0}
    },
    {
      "home": { "team": "Arsenal", "goals" : 0},
      "away": { "team": "Manchester City", "goals" : 1}
    },
    {
      "home": { "team": "Manchester City", "goals" : 1},
      "away": { "team": "Liverpool", "goals" : 1}
    },
    {
      "home": { "team": "Liverpool", "goals" : 1},
      "away": { "team": "Manchester City", "goals" : 4}
    }          
  ],
  2022:
  [
    {
      "home": { "team": "Liverpool", "goals" : 4},
      "away": { "team": "Arsenal", "goals" : 0}
    },
    {
      "home": { "team": "Arsenal", "goals" : 0},
      "away": { "team": "Liverpool", "goals" : 2}
    },
    {
      "home": { "team": "Manchester City", "goals" : 5},
      "away": { "team": "Arsenal", "goals" : 0}
    },
    {
      "home": { "team": "Arsenal", "goals" : 1},
      "away": { "team": "Manchester City", "goals" : 2}
    },
    {
      "home": { "team": "Manchester City", "goals" : 2},
      "away": { "team": "Liverpool", "goals" : 2}
    },
    {
      "home": { "team": "Liverpool", "goals" : 2},
      "away": { "team": "Manchester City", "goals" : 2}
    }          
  ],
  2023:
  [
    {
      "home": { "team": "Liverpool", "goals" : 1},
      "away": { "team": "Arsenal", "goals" : 1}
    },
    {
      "home": { "team": "Arsenal", "goals" : 3},
      "away": { "team": "Liverpool", "goals" : 1}
    },
    {
      "home": { "team": "Manchester City", "goals" : 0},
      "away": { "team": "Arsenal", "goals" : 0}
    },
    {
      "home": { "team": "Arsenal", "goals" : 1},
      "away": { "team": "Manchester City", "goals" : 0}
    },
    {
      "home": { "team": "Manchester City", "goals" : 1},
      "away": { "team": "Liverpool", "goals" : 1}
    },
    {
      "home": { "team": "Liverpool", "goals" : 1},
      "away": { "team": "Manchester City", "goals" : 1}
    }          
  ]

},


The currentData property is good as it is - it's a placeholder for any subset of data we will later derive from data. Let's go back to the HTML. Add a div before each drop-down list, and style it using the teamLogo CSS class.
<div id="teamsContainer">
  <div class="left">
    <div class="teamLogo">
      
    </div>
    <br />

    <select class="ddlTeam">
    
    </select>
  </div>
  <div class="right">
    <div class="teamLogo">
      
    </div>
    <br />

    <select class="ddlTeam">
    
    </select>
  </div>
</div>


Here's the styling in the CSS. The width and height are specified, and the background properties are meant to prepare the div for displaying team logos.
.right
{
  width: 49%;
  float: right;
}

.teamLogo
{
  width: 80%;
  height: 150px;
  margin: 0 auto 0 auto;
  background-repeat: no-repeat;
  background-size: contain;
  background-position: 50% 50%;
}


.ddlTeam
{
  width: 80%;
  height: 20%;
  text-align: center;
  margin: 0 auto 0 auto;
  border-radius: 3px;
  padding: 2px;
  display: block;
}


This should give you a good idea of what to expect.

Now, outside of the h2h object, here's some more code. Begin by declaring ddlTeam. Use the selectAll() method of d3 and pass in the class name of ddlTeam to get an array containing both drop-down lists.
    currentData:
    {
    
    }
  };

  var ddlTeam = d3.selectAll(".ddlTeam");
</script>


Get into the option tags of both drop-down lists by using selectAll() on ddlTeam. Then use the data() method to ensure that we'll be going through the teams property of the h2h object. We'll follow up with the enter() method.
var ddlTeam = d3.selectAll(".ddlTeam");

ddlTeam.selectAll("option")
.data(h2h.teams)
.enter()


And from this point, we'll append an option tag.
var ddlTeam = d3.selectAll(".ddlTeam");

ddlTeam.selectAll("option")
.data(h2h.teams)
.enter()
.append("option")


We then ensure that the first option is selected by default.
var ddlTeam = d3.selectAll(".ddlTeam");

ddlTeam.selectAll("option")
.data(h2h.teams)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})


We make sure that the value attribute and text of the option are the name property of each element in teams.
var ddlTeam = d3.selectAll(".ddlTeam");

ddlTeam.selectAll("option")
.data(h2h.teams)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d.name;
})
.text(function(d)
{
    return d.name;
});


So here it is now! Both drop-down lists should have this list of teams.


At the end of this, run the changeTeams() method of the h2h object. We'll create that soon.
var ddlTeam = d3.selectAll(".ddlTeam");

ddlTeam.selectAll("option")
.data(h2h.teams)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d.name;
})
.text(function(d)
{
    return d.name;
});

h2h.changeTeams();


Within the h2h object is where we create this. We have the logos and the drop-down lists obtained via the select() method of the d3 object, and assigned to variables.
    currentData:
    {
    
    },
    changeTeams: function()
    {
      var imgTeam1 = d3.select("#team1Logo");
      var ddlTeam1 = d3.select("#ddlTeam1");

      var imgTeam2 = d3.select("#team2Logo");
      var ddlTeam2 = d3.select("#ddlTeam2");
    }

  };
</script>


We then iterate through the teams array using a For loop. In the loop, we check if the current element's name property is equal to the first drop-down list's option tag's value attribute.
changeTeams: function()
{
  var imgTeam1 = d3.select("#team1Logo");
  var ddlTeam1 = d3.select("#ddlTeam1");

  var imgTeam2 = d3.select("#team2Logo");
  var ddlTeam2 = d3.select("#ddlTeam2");

  for (var i = 0; i < this.teams.length; i++)
  {
    if (ddlTeam1.node().value == this.teams[i].name)
  }

}


And if so, we set the first logo's background-image property to the current element's logo property.
changeTeams: function()
{
  var imgTeam1 = d3.select("#team1Logo");
  var ddlTeam1 = d3.select("#ddlTeam1");

  var imgTeam2 = d3.select("#team2Logo");
  var ddlTeam2 = d3.select("#ddlTeam2");

  for (var i = 0; i < this.teams.length; i++)
  {
    if (ddlTeam1.node().value == this.teams[i].name) imgTeam1.attr("style", "background-image: url(" + this.teams[i].logo + ")");
  }
}


We do the same for other drop-down list and logo.
changeTeams: function()
{
  var imgTeam1 = d3.select("#team1Logo");
  var ddlTeam1 = d3.select("#ddlTeam1");

  var imgTeam2 = d3.select("#team2Logo");
  var ddlTeam2 = d3.select("#ddlTeam2");

  for (var i = 0; i < this.teams.length; i++)
  {
    if (ddlTeam1.node().value == this.teams[i].name) imgTeam1.attr("style", "background-image: url(" + this.teams[i].logo + ")");
    if (ddlTeam2.node().value == this.teams[i].name)imgTeam2.attr("style", "background-image: url(" + this.teams[i].logo + ")");
  }
}


Of course, none of this will work if we don't put in the appropriate id attributes. And add the changeTeams() method to the conchange attribute of both drop-down lists.
<div id="teamsContainer">
  <div class="left">
    <div class="teamLogo" id="team1Logo">
      
    </div>
    <br />
    <select class="ddlTeam" id="ddlTeam1" onchange="h2h.changeTeams()">

    </select>
  </div>
  <div class="right">
    <div class="teamLogo" id="team2Logo">
      
    </div>
    <br />
    <select class="ddlTeam" id="ddlTeam2" onchange="h2h.changeTeams()">

    </select>
  </div>
</div>


By default the first is always selected, and the first team happens to be Arsenal.


Now I change the teams of each drop-down list, and this happens!


Let's handle a same-team scenario!

Now, when the same team is selected on each drop-down list, logically there can't be any data displayed because teams don't play against themselves in a Premier League Season. So let's handle that. Add a call to the getData() method inside changeTeams().
changeTeams: function()
{
  var imgTeam1 = d3.select("#team1Logo");
  var ddlTeam1 = d3.select("#ddlTeam1");

  var imgTeam2 = d3.select("#team2Logo");
  var ddlTeam2 = d3.select("#ddlTeam2");

  for (var i = 0; i < this.teams.length; i++)
  {
    if (ddlTeam1.node().value == this.teams[i].name) imgTeam1.attr("style", "background-image: url(" + this.teams[i].logo + ")");
    if (ddlTeam2.node().value == this.teams[i].name)imgTeam2.attr("style", "background-image: url(" + this.teams[i].logo + ")");
  }

  this.getData();
}


Create the method.
currentData: [],
getData: function()
{

},

changeTeams: function()
{
  var imgTeam1 = d3.select("#team1Logo");
  var ddlTeam1 = d3.select("#ddlTeam1");

  var imgTeam2 = d3.select("#team2Logo");
  var ddlTeam2 = d3.select("#ddlTeam2");

  for (var i = 0; i < this.teams.length; i++)
  {
    if (ddlTeam1.node().value == this.teams[i].name) imgTeam1.attr("style", "background-image: url(" + this.teams[i].logo + ")");
    if (ddlTeam2.node().value == this.teams[i].name)imgTeam2.attr("style", "background-image: url(" + this.teams[i].logo + ")");
  }

  this.getData();
}


In here, we begin much like we did in changeTeams().
getData: function()
{
  var ddlTeam1 = d3.select("#ddlTeam1");
  var ddlTeam2 = d3.select("#ddlTeam2");

  var team1 = ddlTeam1.node().value;
  var team2 = ddlTeam2.node().value;

},


Then we ensure that the currentData property is set to an empty array. This will be filled in as data is found.
getData: function()
{
  var ddlTeam1 = d3.select("#ddlTeam1");
  var ddlTeam2 = d3.select("#ddlTeam2");

  var team1 = ddlTeam1.node().value;
  var team2 = ddlTeam2.node().value;
  this.currentData = [];
},


However, if both drop-down lists have the same value, we exit early and currentData remains an empty array.
getData: function()
{
  var ddlTeam1 = d3.select("#ddlTeam1");
  var ddlTeam2 = d3.select("#ddlTeam2");

  var team1 = ddlTeam1.node().value;
  var team2 = ddlTeam2.node().value;
  this.currentData = [];

  if (team1 == team2) return;
},


Now add a call to renderLineChart().
changeTeams: function()
{
  var imgTeam1 = d3.select("#team1Logo");
  var ddlTeam1 = d3.select("#ddlTeam1");

  var imgTeam2 = d3.select("#team2Logo");
  var ddlTeam2 = d3.select("#ddlTeam2");

  for (var i = 0; i < this.teams.length; i++)
  {
    if (ddlTeam1.node().value == this.teams[i].name) imgTeam1.attr("style", "background-image: url(" + this.teams[i].logo + ")");
    if (ddlTeam2.node().value == this.teams[i].name)imgTeam2.attr("style", "background-image: url(" + this.teams[i].logo + ")");
  }

  this.getData();

  this.renderLineChart();
}


Create renderLineChart(). In it, we start by declaring chart and setting it to the SVG whose id is chart. And then using the html() method to clear it.
changeTeams: function()
{
  var imgTeam1 = d3.select("#team1Logo");
  var ddlTeam1 = d3.select("#ddlTeam1");

  var imgTeam2 = d3.select("#team2Logo");
  var ddlTeam2 = d3.select("#ddlTeam2");

  for (var i = 0; i < this.teams.length; i++)
  {
    if (ddlTeam1.node().value == this.teams[i].name) imgTeam1.attr("style", "background-image: url(" + this.teams[i].logo + ")");
    if (ddlTeam2.node().value == this.teams[i].name)imgTeam2.attr("style", "background-image: url(" + this.teams[i].logo + ")");
  }

  this.getData();

  this.renderLineChart();
},
renderLineChart: function()
{
  var chart = d3.selectAll("#chart");
  chart.html("");
}


We then check if currentData is an empty array.
renderLineChart: function()
{
  var chart = d3.selectAll("#chart");
  chart.html("");

  if (this.currentData.length == 0)
  {

  }

}


In chart, we pretty much just append a text tag (styled using the nodata CSS class) into the SVG with a message...
renderLineChart: function()
{
  var chart = d3.selectAll("#chart");
  chart.html("");

  if (this.currentData.length == 0)
  {
    chart
    .append("text")
    .attr("class", "nodata")
    .attr("x", "400px")
    .attr("y", "200px")
    .text("No data. Please select another pair of teams.");

  }
}


...and exit early.
renderLineChart: function()
{
  var chart = d3.selectAll("#chart");
  chart.html("");

  if (this.currentData.length == 0)
  {
    chart
    .append("text")
    .attr("class", "nodata")
    .attr("x", "400px")
    .attr("y", "200px")
    .text("No data. Please select another pair of teams.");
    return;
  }
}


In the CSS, we style nodata with font, color and alignment.
#chartContainer, #chart
{
  width: 800px;
  height: 400px;
  margin: 0 auto 0 auto;
}

.nodata
{
  font: bold 30px arial;
  fill: rgba(0, 0, 0, 0.5);
  text-anchor: middle;
}


So now, if we select the same team for both drop-down lists, we get a message!


Next

Getting and visualizing data for different teams.