import {Plot} from "npm:@observablehq/plot"
import {FileAttachment} from "npm:@observablehq/stdlib"
import * as d3 from "npm:d3@7"
import {Inputs} from "npm:@observablehq/inputs"
import {html} from "npm:htl"
const fifaRawData = await FileAttachment("fifa_teams_cleaned.csv").csv({typed: true});
const matchRawData = await FileAttachment("match_teams.csv").csv({typed: true});
function convertCurrencyToNumeric(value) {
if (typeof value !== 'string') return 0;
let cleanValue = value.replace(/[€£$\s]/g, '').toUpperCase();
if (cleanValue.endsWith('K')) {
return parseFloat(cleanValue.replace('K', '')) * 1000;
} else if (cleanValue.endsWith('M')) {
return parseFloat(cleanValue.replace('M', '')) * 1000000;
} else if (cleanValue.endsWith('B')) {
return parseFloat(cleanValue.replace('B', '')) * 1000000000;
} else {
return parseFloat(cleanValue) || 0;
}
}
const attributeScales = {
Speed: { 'Slow': 25, 'Balanced': 50, 'Fast': 75 },
Passing: { 'Safe': 25, 'Normal': 50, 'Risky': 75 },
Positioning: { 'Free form': 25, 'Organised': 75 },
Aggression: { 'Contain': 25, 'Press': 75 },
Pressure: { 'Deep': 25, 'Medium': 50, 'High': 75 },
'Team width': { 'Narrow': 25, 'Normal': 50, 'Wide': 75 }
};
function derivePlayingStyle(team) {
const speed = team.Speed;
const passing = team.Passing;
const width = team['Team width'];
const pressure = team.Pressure;
if (speed === 'Slow' && passing === 'Safe') {
return 'Defensive';
} else if (width === 'Wide') {
return 'Wing Play';
} else if (passing === 'Normal' && width === 'Normal') {
return 'Balanced';
} else if (pressure === 'High') {
return 'High Press';
} else {
return 'Possession';
}
}
const fifaData = fifaRawData
.filter(d => d.Overall !== null && d.Attack !== null)
.map(d => {
const numericAttributes = {};
Object.entries(attributeScales).forEach(([attr, scale]) => {
if (d[attr] && scale[d[attr]]) {
numericAttributes[attr] = scale[d[attr]];
} else {
numericAttributes[attr] = 50;
}
});
return {
...d,
...numericAttributes,
transferBudget: convertCurrencyToNumeric(d['Transfer budget']),
clubWorth: convertCurrencyToNumeric(d['Club worth']),
playingStyle: derivePlayingStyle(d),
technicalSkill: (numericAttributes.Passing + (d.Midfield || 70)) / 2,
physicalIndex: (numericAttributes.Speed + numericAttributes.Aggression) / 2,
isRecent: d.date === "2024-07-08"
};
});
const matchData = matchRawData
.filter(d => d.Pts !== null && d.name !== null)
.map(d => ({
...d,
country: d.name.split(' ')[0],
fifaId: d.sofifa_id
}));
const recentFifaData = fifaData.filter(d => d.isRecent);
const uniqueLeagues = [...new Set(matchData.map(d => d.league))]
.filter(d => d !== null)
.sort((a, b) => a - b);
const leagueOptions = uniqueLeagues.map(id => {
const count = matchData.filter(d => d.league === id).length;
return { id, label: `League ${id} (${count} teams)` };
});
const playingStyles = [...new Set(recentFifaData.map(d => d.playingStyle))];
let selectedTeamID = null;
let selectedLeagueID = 'all';
let selectedStyleName = 'All Styles';
let selectedSortMetric = 'Overall';
viewof leagueFilter = Inputs.select(
[{ id: 'all', label: 'All Leagues' }, ...leagueOptions],
{
label: "Filter by League",
value: selectedLeagueID
}
);
viewof styleFilter = Inputs.select(
['All Styles', ...playingStyles],
{
label: "Filter by Playing Style",
value: selectedStyleName
}
);
viewof sortMetric = Inputs.select(
['Overall', 'Attack', 'Midfield', 'Defence', 'clubWorth', 'transferBudget', 'technicalSkill', 'physicalIndex'],
{
label: "Sort Teams By",
value: selectedSortMetric
}
);
leagueFilter;
styleFilter;
sortMetric;
selectedLeagueID = leagueFilter;
selectedStyleName = styleFilter;
selectedSortMetric = sortMetric;
function highlightTeam(teamID) {
selectedTeamID = teamID;
document.querySelectorAll(".highlight-team").forEach(el => {
el.classList.remove("highlight-team");
});
if (teamID) {
document.querySelectorAll(`[data-team="${teamID}"]`).forEach(el => {
el.classList.add("highlight-team");
});
}
}
function getFilteredTeams() {
let filtered = recentFifaData;
if (selectedStyleName !== 'All Styles') {
filtered = filtered.filter(d => d.playingStyle === selectedStyleName);
}
if (selectedLeagueID !== 'all') {
const leagueId = selectedLeagueID.id || parseInt(selectedLeagueID);
const teamsInLeague = matchData.filter(d => d.league === leagueId);
if (teamsInLeague.some(d => d.sofifa_id !== null)) {
const fifaIds = new Set(teamsInLeague.map(d => d.sofifa_id).filter(id => id !== null));
filtered = filtered.filter(d => fifaIds.has(d.ID));
}
}
return filtered;
}
const filteredTeams = getFilteredTeams();
const styles = html`<style>
.highlight-team {
stroke: #ff5500 !important;
stroke-width: 3px !important;
}
.team-tooltip {
font-size: 12px;
padding: 8px;
background: rgba(255, 255, 255, 0.9);
border: 1px solid #ccc;
border-radius: 4px;
}
</style>`;