Public
Edited
Jun 18, 2023
1 fork
Insert cell
Insert cell
Insert cell
metrics = [
"possession",
"total attempts",
"on target attempts",
"passes",
"passes completed",
"corners",
"offsides",
"yellow cards",
"red cards"
// Add more metrics as needed from the Fifa_world_cup_matches@4.csv
];
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
fetch('https://raw.githubusercontent.com/vasturiano/globe.gl/master/example/datasets/ne_110m_admin_0_countries.geojson').then(res => res.json())
Insert cell
Insert cell
Insert cell
Insert cell
function setMatchStats(stats, selection, containerWidth, containerHeight) {
const margin = { top: 0, right: 20, bottom: 20, left: 20 };
const chartWidth = containerWidth - margin.left - margin.right;
const chartHeight = containerHeight - margin.top - margin.bottom;

// get colors
const colorTeam1 = getColorByCountry(stats.metadata.nameTeam1);
const colorTeam2 = getColorByCountry(stats.metadata.nameTeam2);
const invColorTeam1 = getInverseColor(colorTeam1, 0.5);
const invColorTeam2 = getInverseColor(colorTeam2, 0.5);

// prepend stuff to stats.data to have space for the title when doing the scaleBand()
stats.data.unshift(
{Metric: "datetime-category"},
{Metric: "names"},
{Metric: "score"}
)
// Define the scales for x and y axes
const xScale = d3.scaleLinear().domain([-1, 1]).range([0, chartWidth]);
const yScale = d3
.scaleBand()
.domain(stats.data.map((d) => d.Metric))
.range([0.5, chartHeight])
.padding(0.6);

// util functions
const denormalise = (d, total) => total * d;
const numFormatWrapper = (val, numType) => {
if (numType === "percentage") {
return d3.format(".1%")(val/100);
} else if (numType === "integer") {
return Math.round(val);
} else if (numType === "float") {
return d3.format(".1")(val);
} else {
return val;
}
}

const lrWrapper = (countryName, left) => {
const emojiStr = getWinnersEmoji(countryName)
if (emojiStr.length == 0) {
return countryName;
} else if (left) {
return `${emojiStr} ${countryName}`;
} else {
return `${countryName} ${emojiStr}`;
}
}

// get main SVG container
const svg = selection
.append("svg")
.attr("width", containerWidth)
.attr("height", containerHeight);
// Create a group for the chart
const chart = svg
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);

// add category and datetime
chart
.append("text")
.attr("class", "datetime-cat-stats")
.attr("x", d => (xScale(-1)) )
.attr("y", d => yScale(stats.data[0].Metric))
.attr("text-anchor", "start")
.attr("alignment-baseline", "middle")
.text(stats.metadata.category);

chart
.append("text")
.attr("class", "datetime-cat-stats")
.attr("x", d => (xScale(1)))
.attr("y", d => yScale(stats.data[0].Metric))
.attr("text-anchor", "end")
.attr("alignment-baseline", "middle")
.text(`${stats.metadata.date} - ${stats.metadata.hour}`);

// add country names
chart
.append("text")
.attr("class", "title-stats")
.attr("x", d => (xScale(0 - 0.5)) )
.attr("y", d => yScale(stats.data[1].Metric))
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("fill", colorTeam1)
.text(lrWrapper(stats.metadata.nameTeam1, true));

chart
.append("text")
.attr("class", "title-stats")
.attr("x", d => (xScale(0 + 0.5)))
.attr("y", d => yScale(stats.data[1].Metric))
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("fill", colorTeam2)
.text(lrWrapper(stats.metadata.nameTeam2));

// add scores
const [scoreTeam1, scoreTeam2] = stats.metadata.score
chart
.append("text")
.attr("class", "score-stats")
.attr("x", d => (xScale(0) - 10))
.attr("y", d => yScale(stats.data[2].Metric))
.attr("text-anchor", "end")
.attr("alignment-baseline", "middle")
.text(scoreTeam1);

chart
.append("text")
.attr("class", "score-stats")
.attr("x", d => (xScale(0)))
.attr("y", d => yScale(stats.data[2].Metric))
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("fill", "white")
.text("-");
chart
.append("text")
.attr("class", "score-stats")
.attr("x", d => (xScale(0) + 10))
.attr("y", d => yScale(stats.data[2].Metric))
.attr("text-anchor", "start")
.attr("alignment-baseline", "middle")
.text(scoreTeam2);
// remove non-barplot stuff
stats.data.shift() // datetime-category
stats.data.shift() // names
stats.data.shift() // score
// Create the bars
const lbars = chart
.selectAll("team1-rect")
.data(stats.data)
.enter()
.append("rect")
.attr("class", "bar rect-stats")
.attr("x", d => xScale(0 - d.team1))
.attr("y", d => yScale(d.Metric))
.attr("width", d => Math.abs(xScale(d.team1) - xScale(0)))
.attr("height", yScale.bandwidth())
.attr("fill", colorTeam1);

const rbars = chart
.selectAll("team2-rect")
.data(stats.data)
.enter()
.append("rect")
.attr("class", "bar rect-stats")
.attr("x", d => xScale(0))
.attr("y", d => yScale(d.Metric))
.attr("width", d => Math.abs(xScale(d.team2) - xScale(0)))
.attr("height", yScale.bandwidth())
.attr("fill", colorTeam2);

// Add metric labels
chart
.selectAll(".metric-label")
.data(stats.data)
.enter()
.append("text")
.attr("class", "text-rect-stats")
.attr("x", d => (xScale(0)))
.attr("y", d => yScale(d.Metric) - yScale.bandwidth() / 2)
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("fill", "white")
.text(d => d.Metric);
// Add labels for team1 values
chart
.selectAll(".team1-label")
.data(stats.data)
.enter()
.append("text")
.attr("class", "text-rect-stats")
.attr("x", d => (xScale(0)) - 10)
.attr("y", d => yScale(d.Metric) + yScale.bandwidth() / 1.75)
.attr("text-anchor", "end")
.attr("alignment-baseline", "middle")
.attr("fill", invColorTeam1)
.text(d => numFormatWrapper(denormalise(d.team1, d.total), d.numType));


// Add labels for team2 values
chart
.selectAll(".metric-label")
.data(stats.data)
.enter()
.append("text")
.attr("class", "text-rect-stats")
.attr("x", d => (xScale(0)) + 10)
.attr("y", d => yScale(d.Metric) + yScale.bandwidth() / 1.75)
.attr("text-anchor", "start")
.attr("alignment-baseline", "middle")
.attr("fill", invColorTeam2)
.text(d => numFormatWrapper(denormalise(d.team2, d.total), d.numType));
return svg.node();
}
Insert cell
Insert cell
Fifa_world_cup_matches@4.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
country_colours@6.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
concap@3.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
capFirstLetter("SOUTH KOREA")
Insert cell
Insert cell
remapNameIfNeeded("Mexico")
Insert cell
Insert cell
Insert cell
Insert cell
getMatchesSummary("Argentina")
Insert cell
Insert cell
getGoalsSummary("Mexico")
Insert cell
Insert cell
getColorByCountry("United States") //, 0.5)
Insert cell
Insert cell
getInverseColor(getColorByCountry("Argentina"))
Insert cell
Insert cell
getPoVByCountry("Mexico", 2.5)
Insert cell
Insert cell
getArcsData("Mexico", 0.9)
Insert cell
Insert cell
function getMatchStats(team1, team2, date) {
date = date || "";

let teamMatch;
if (date.length === 0) {
// no date nor hour provided, use only team1 and team2 to find match
teamMatch = fifa_world_cup_matches.filter(row => {
return (
(row['team1'] === team1.toUpperCase() && row['team2'] === team2.toUpperCase()) ||
(row['team1'] === team2.toUpperCase() && row['team2'] === team1.toUpperCase())
);
});
} else {
// no date provided, use only team1 and team2 to find match
teamMatch = fifa_world_cup_matches.filter(row => {
return (
(
(row['team1'] === team1.toUpperCase() && row['team2'] === team2.toUpperCase()) ||
(row['team1'] === team2.toUpperCase() && row['team2'] === team1.toUpperCase())
) && (row['date'] === date)
);
});
}

const match = teamMatch[0]
const team1Key = (team1.toUpperCase() === match['team1']) ? 'team1' : 'team2'
const team2Key = (team1.toUpperCase() === match['team1']) ? 'team2' : 'team1'
const normalise = (d, total) => total === 0 ? 0 : d/total;
const determineNumberType = (str) => {
if (/%$/.test(str)) {
return "percentage";
} else if (/^\d+$/.test(str)) {
return "integer";
} else if (/^\d+(\.\d+)?$/.test(str)) {
return "float";
} else {
return "unknown";
}
}

// team1 will be the selectedTeam and team2 the opponentTeam
const result = metrics.map(metric => {
const team1Value = parseFloat(teamMatch[0][`${metric} ${team1Key}`]);
const team2Value = parseFloat(teamMatch[0][`${metric} ${team2Key}`]);
const totalValue = team1Value + team2Value;
return {
Metric: metric,
team1: isNaN(team1Value) ? null : normalise(team1Value, totalValue),
team2: isNaN(team2Value) ? null : normalise(team2Value, totalValue),
total: isNaN(totalValue) ? null : totalValue,
numType: isNaN(totalValue) ? null : determineNumberType(teamMatch[0][`${metric} team1`]),
}
});

return {
metadata: {
nameTeam1: team1,
nameTeam2: team2,
date: teamMatch[0].date,
hour: teamMatch[0].hour,
category: teamMatch[0].category,
score: getMatchScoreWithEmojis(match, team1Key, team2Key)
},
data: result
};

}
Insert cell
getMatchStats("Argentina", "France")
Insert cell
Insert cell
Insert cell
Insert cell
svgField(0.5)
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more