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