Wednesday, 27 November 2024

Spot The Bug: The One-week Chinese Calendar

Hay hello to Spot The Bug, programmers! And have I got a doozy for you!

Damn these bugs!
They're everywhere!


I had this idea to make a calendar interface that would display the date... in Chinese. Like a Chinese calendar. So I broke out some HTML and CSS, and began populating it with JavaScript. It was the start of July this year. That fact will soon be relevant.

I ended up with this code. Upon page load, it should take today's date and render it in the style I wanted.
<!DOCTYPE html>
<html>
  <head>
    <title>Chinese Calendar</title>

    <style>
      .calframe
      {
        width: 250px;
        height: 250px;
        margin: 10% auto 0 auto;
        border: 3px solid #009900;
        color: #00AA00;
      }

      .calframe div
      {
        width: 100%;
        padding: 5px;      
      }

      #month
      {
        font-size: 1em;
      }

      #day
      {
        text-align: center;
        font-size: 4.5em;
        font-weight: bold;
      }
    </style>

    <script>
      function replaceNumbers()
      {
        var numbers =
        [
          "一", "二", "三", "四", "五", "六", "七", "八", "九", "十",
          "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
          "二十一", "二十二", "二十三", "二十四", "二十五", "二十六", "二十七", "二十八", "二十九", "三十",
          "三十一"
        ];

        var d = new Date();
        var month = d.getMonth();
        var day = d.getDay();

        document.getElementById("month").innerHTML = numbers[month] + "月";
        document.getElementById("day").innerHTML = numbers[day - 1];      

      }
    </script>
  </head>

  <body onload="replaceNumbers();">
    <div class="calframe">
      <div id="month"></div>
      <div id="day"></div>
    </div>
  </body>
</html>


And it worked too... well, for about a week. This was what it looked like on 4th July.


What Went Wrong

After a week, on 8th of July, it showed this. In Chinese, 1st July.


And 9th July. In Chinese, 2nd July.


10th July. In Chinese, 3rd July.


Why It Went Wrong

The fact that it worked for the first week, gave me a clue. I did some reading on the getDay() method in JavaScript. Turns out it was getting the day of the week instead of the day of the month!

This was why it worked on the first week - because 1st July started on a Monday! And 2nd July was on a Tuesday, and so on. So even though the output was wrong, it looked correct. Until the second week rolled around.

How I Fixed It

All I needed to do was change the getDay() method call to getDate().
var day = d.getDate();


And now it worked! It showed 10th July in Chinese... on 10th July!


Moral of the Story

This isn't the first time I've ever been fooled by the name of a method, with comical results. It's tempting to blame badly-named methods for this. As every programmer knows, naming things is pretty friggin' hard.

Still, it's on me. I should have been more aware of the methods I was attempting to use.

Good day (or date) to you!
T___T

Saturday, 23 November 2024

A Software Developer's Vacation in Malacca

Somewhere in the past month, I was flirting with the idea of taking a trip to Malacca City in Malacca, Malaysia. The wife was away, and I basically had free reign. One of my friends from Kuala Lumpur that I'd met on the Clubhouse app, had a counter-suggestion - we could meet up in Kuala Lumpur and take a day trip to Melaka!

Didn't sound too bad an idea, to be honest. I could use the company. And a local guide could help point out all the beautiful places I might miss.


My company was entering the busiest time of the entire year - the December festive period - and if I wanted to clear any leave at all, it was going to be then. Fingers crossed, my colleagues wouldn't bother me much during my trip. Being the head of a one-man Infocomm department comes with inherent liability, and these were the risks I had to manage.

The trip begins!

I arrived in Kuala Lumpur to spend the night in a hotel room, then my buddy came over in the morning in a charmingly beat-up Toyota the likes of I haven't seen since childhood. We had a simple and leisurely breakfast, then headed off. The morning traffic was horrendous until we hit the highway, and then we were looking at miles of open road as the palm trees whizzed by.



When we finally arrived in Malacca after a couple hours, I was greeted by the sight of what looked like a huge inflatable dragon coiling around the main archway. It was a weird cross between cheesy and oddly majestic. It did occur to me that this area looked strikingly Chinese.

The day was hot, but due to a sudden shower an hour before, the ground was wet with puddles. Not a great combination.

Jonker Street 

Now this was one part that reminded me of Armenian Street in Penang's Georgetown. Tourists milling around was a dead giveaway, but also the little quaint cafes here and there that looked too "touristy" to be authentic. The numerous shops selling all manner of Malaysia-themed knick-knacks was also a huge callback to my time in Penang.


I succumbed to the temptation and bought a refrigerator magnet. And only because it looked hand-made.


Somewhere along the street was this cheesy monument of some seriously ripped dude with this shit-eating grin. To this day, I'm not sure if that bust was built in earnest or mockery. This guy was apparently a bodybuilder in addition to being a politician.


I had the chance to look out along the Malacca River. Nice. Reminded me of Singapore River, except without the glitzed-up banks.

Food 

We had a break at one of the numerous places advertising Vietnam Coffee. Let me just say, for the record, that Vietnam Coffee just isn't Vietnam Coffee if you're not drinking it while squatting on the roadside on a tiny stool in Saigon as motorcycle traffic roars by you... but on a hot day like this, it worked in a pinch.


Later on, we had chicken rice in one of the numerous shops. The specialty appeared to be balls rolled from chicken rice... but honestly I ate it just to be able to say I've done it before. What was more interesting were the bean sprouts - they were large, fat and crunchy. I was told that they were a special breed known as "Ipoh" sprouts. Nice!

Museum and Art Gallery 

Lunch was done and we headed off across the street to this red building, called the Dutch Square. It used to be a fort and was now home to a museum and art gallery, among other things. For lack of something more obvious to do, the museum was first.


Now, honestly, there've been many people who told me to "read a History book" during online arguments, as though that qualified as the final word in any debate. History just happens to be one of my least favorite subjects. What I was about to see, however, was a virtual smorgasbord of violence.



You see, Malacca used to be an object of contention between Malaysians, the Dutch and the Portugese. Apparently the Malacca Straits were very important territory. We went through floor after floor of paintings, text and dioramas depicting acts of bloody battle. Stabbings, shootings, blood and agony, it was all there. Not for the faint of heart, that's for sure.

I saw weapons on display too - sabers and guns, with bullets the size of the chicken rice balls I'd eaten earlier. Military uniforms. Model ships. And my absolute favorite... miniature replicas of Malacca back in the day, depicting how it looked like at different periods in history. I could have stared at this shit for hours.



This was capped off by the sight of this badass statue of Admiral Cheng Ho at the rooftop.



The next part of this visual tour was the art gallery just next door. And, to be honest, there was quite a bit of art on display that I just didn't get. Some of it was historical, and those were actually the easiest to look at. The more abstract pieces? Call me a Philistine, but not so much.

The last part 

After leaving Jonker, we made a side detour. It was a white building by the seaside called the Encore Melaka, and it was an absolutely beautiful theater. Neither of us really felt like shelling out a hundred bucks for about three hours of their entertainment, so we just hung around outside.


And, in my case, took pictures like some tourist. Which I was.


By the sea, this sign proudly proclaimed the area to be The Maritime Silke Road, and even provided coordinates. Shit, this place was all kinds of fancy.

The curse struck again... 

Almost every day, I'd review my messages and see that there was stuff to attend to. Some of it I could defer till I got back; the others I had to attend to by the next day.

Honestly, it was annoying AF. It was like, the effort I'd put into an automatic vacation message and informing my co-workers, just didn't exist.


But this is what happens when you're a one-man Infocomm department - there are no reasonable expectations of a complete day off. Something goes wrong, and even if it could be looked into by someone else, the first instinctual response is to contact me by WhatsApp or send me a MS Teams message.

I'm not blameless in this regard though - I get such a summons and even though I'm supposed to be on vacation, I can't help myself. I just have to respond. This is something I need to seriously work on. As to how, for the moment, I have no clue.

What a vacation!

Short, but sweet. For real, though, this struck me as a good place to chill out for an entire afternoon if I felt like it. Maybe even for a few afternoons. And as of last week (or earlier) there are direct flights from Singapore to Malacca.

Speaking of which, I actually experienced the biometric scan when returning to Singapore's Changi Airport, days after it was reported in the local paper. Pretty cool. Traveling in and out of Singapore as a citizen wasn't a huge hassle to begin with, but this is going to make things even easier.

Stay tuned for more vacation updates!

Your travelling techie,
T___T

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.