Skip to content

Commit 008b363

Browse files
authored
Add files via upload
1 parent 3578f61 commit 008b363

File tree

3 files changed

+452
-1
lines changed

3 files changed

+452
-1
lines changed
Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,36 @@
1-
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>F1 Constructor Standings Dashboard</title>
6+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
7+
<link rel="stylesheet" href="styles.css">
8+
<script src="https://d3js.org/d3.v7.min.js"></script>
9+
</head>
10+
<body>
11+
<div class="dashboard-grid">
12+
<header>
13+
<h1>F1 Constructor Standings Dashboard</h1>
14+
<div class="controls">
15+
<label for="seasonSelect">Select Season:</label>
16+
<select id="seasonSelect"></select>
17+
</div>
18+
</header>
19+
<main>
20+
<div id="chart"></div>
21+
<div id="summaryTable"></div>
22+
</main>
23+
<aside>
24+
<div id="legend"></div>
25+
</aside>
26+
<footer>
27+
<p>
28+
Powered by D3.js • Visualization by Francesco Saviano<br>
29+
Data &copy; <a href="https://www.kaggle.com/datasets/rohanrao/formula-1-world-championship-1950-2020" target="_blank" rel="noopener">Rohan Rao (Kaggle: vopani)</a> – Based on the <a href="https://ergast.com/mrd/" target="_blank" rel="noopener">Ergast Developer API</a>
30+
</p>
31+
</footer>
32+
33+
</div>
34+
<script src="script.js"></script>
35+
</body>
36+
</html>
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
// ---- Official HEX color mapping for F1 constructors (add others if needed) ----
2+
const TEAM_COLORS = {
3+
"Red Bull": "#1e41ff",
4+
"Red Bull Racing": "#1e41ff",
5+
"Ferrari": "#dc0000",
6+
"Mercedes": "#6cd3bf",
7+
"McLaren": "#ff8700",
8+
"Aston Martin": "#229971",
9+
"Alpine": "#2293d1",
10+
"Alpine F1 Team": "#2293d1",
11+
"Williams": "#005aff",
12+
"RB F1 Team": "#3f27c1",
13+
"AlphaTauri": "#2b4562",
14+
"Toro Rosso": "#0032a0",
15+
"Sauber": "#52e4c2",
16+
"Alfa Romeo": "#900000",
17+
"Haas F1 Team": "#b6babd",
18+
"Racing Point": "#e5c8d2",
19+
"Force India": "#f596c8",
20+
"Lotus": "#145a32",
21+
"Lotus F1": "#145a32",
22+
"Renault": "#ffd800",
23+
"Benetton": "#00b950",
24+
"Minardi": "#1a1a1a",
25+
"Super Aguri": "#e30613",
26+
"Jaguar": "#007a3d",
27+
"Toyota": "#eb0a1e",
28+
"Manor Marussia": "#c3002f",
29+
"Marussia": "#c3002f",
30+
"Caterham": "#005f3c",
31+
"HRT": "#f7d117",
32+
"Virgin": "#e20613",
33+
"Brawn": "#fff600",
34+
"BMW Sauber": "#0057b8",
35+
"BMW": "#0057b8",
36+
"Jordan": "#ffd700",
37+
"BAR": "#d50032",
38+
"Prost": "#003399",
39+
"Stewart": "#0055a4",
40+
"Arrows": "#ff7f00",
41+
"Ligier": "#1976d2",
42+
"Footwork": "#ff003c",
43+
"Osella": "#004466",
44+
"Zakspeed": "#e70c27",
45+
"Shadow": "#343434",
46+
"Surtees": "#0033cc",
47+
"Matra": "#1976d2",
48+
"Tyrrell": "#1e88e5",
49+
"March": "#a7d5eb",
50+
"Brabham": "#1c1c1c",
51+
"BRM": "#00693e",
52+
"Cooper": "#004225",
53+
"Vanwall": "#367047",
54+
"Honda": "#e50012",
55+
"Porsche": "#c0c0c0"
56+
};
57+
58+
// ---- Fallback palette HEX for rare/unknown teams (will never duplicate with official) ----
59+
const PALETTE = [
60+
"#ff9900", "#a259c2", "#f06292", "#4dd0e1", "#8bc34a", "#ffe600", "#8d5524", "#e17055",
61+
"#1abc9c", "#2ecc71", "#f1c40f", "#34495e", "#9b59b6", "#c0392b", "#2980b9", "#d35400"
62+
];
63+
64+
// --- Assign a unique color to each team (official first, then fallback palette) ---
65+
function assignColorsToTeams(teamNames) {
66+
let mapping = {};
67+
let usedColors = new Set();
68+
let paletteIdx = 0;
69+
70+
// Official HEX color if present
71+
teamNames.forEach(team => {
72+
if (TEAM_COLORS[team]) {
73+
mapping[team] = TEAM_COLORS[team];
74+
usedColors.add(TEAM_COLORS[team]);
75+
}
76+
});
77+
78+
// Assign remaining teams a palette color, skipping duplicates
79+
teamNames.forEach(team => {
80+
if (!mapping[team]) {
81+
while (usedColors.has(PALETTE[paletteIdx]) && paletteIdx < PALETTE.length) paletteIdx++;
82+
mapping[team] = PALETTE[paletteIdx] || "#888888";
83+
usedColors.add(mapping[team]);
84+
paletteIdx++;
85+
}
86+
});
87+
88+
return mapping;
89+
}
90+
91+
// --- Load all required CSV data and initialize dashboard ---
92+
Promise.all([
93+
d3.csv('data/seasons.csv'),
94+
d3.csv('data/races.csv'),
95+
d3.csv('data/constructors.csv'),
96+
d3.csv('data/constructor_standings.csv')
97+
]).then(function([seasons, races, constructors, standings]) {
98+
const select = d3.select("#seasonSelect");
99+
const years = seasons.map(d => d.year).sort((a, b) => +b - +a);
100+
select.selectAll("option")
101+
.data(years)
102+
.enter()
103+
.append("option")
104+
.attr("value", d => d)
105+
.text(d => d);
106+
renderDashboard(years[0], races, constructors, standings);
107+
108+
select.on("change", function() {
109+
renderDashboard(this.value, races, constructors, standings);
110+
});
111+
});
112+
113+
// --- Main dashboard rendering for a selected season ---
114+
function renderDashboard(season, races, constructors, standings) {
115+
// Filter races and standings for the chosen season
116+
const seasonRaces = races.filter(r => r.year === season)
117+
.sort((a, b) => +a.round - +b.round);
118+
const raceIds = seasonRaces.map(r => r.raceId);
119+
const seasonStandings = standings.filter(s => raceIds.includes(s.raceId));
120+
const constructorMap = {};
121+
constructors.forEach(c => { constructorMap[c.constructorId] = c.name; });
122+
123+
// List all teams present for this season
124+
let teamsSet = new Set();
125+
seasonStandings.forEach(s => teamsSet.add(constructorMap[s.constructorId] || s.constructorId));
126+
const teamNames = Array.from(teamsSet);
127+
128+
// Assign consistent colors for this season
129+
const teamColors = assignColorsToTeams(teamNames);
130+
131+
// Build data structure: {team: [{round, points}]}
132+
const teamData = {};
133+
seasonStandings.forEach(s => {
134+
const team = constructorMap[s.constructorId] || s.constructorId;
135+
if (!teamData[team]) teamData[team] = [];
136+
teamData[team].push({
137+
raceId: s.raceId,
138+
round: seasonRaces.find(r => r.raceId === s.raceId)?.round || 0,
139+
points: +s.points
140+
});
141+
});
142+
143+
// Fill 0s for races without points and calculate cumulative points
144+
Object.keys(teamData).forEach(team => {
145+
teamData[team] = seasonRaces.map(race => {
146+
const data = teamData[team].find(d => d.raceId === race.raceId);
147+
return { round: +race.round, points: data ? data.points : 0 };
148+
});
149+
// Cumulative sum over rounds
150+
teamData[team].sort((a, b) => a.round - b.round);
151+
for (let i = 1; i < teamData[team].length; i++) {
152+
if (teamData[team][i].points < teamData[team][i-1].points)
153+
teamData[team][i].points += teamData[team][i-1].points;
154+
}
155+
});
156+
157+
drawLineChart(teamData, seasonRaces, season, teamColors);
158+
drawLegend(teamNames, teamColors);
159+
drawSummaryTable(teamData, teamColors);
160+
}
161+
162+
// --- Draw legend for all teams with correct colors ---
163+
function drawLegend(teamNames, teamColors) {
164+
const legendDiv = d3.select("#legend");
165+
legendDiv.html("");
166+
teamNames.forEach(team => {
167+
legendDiv.append("div")
168+
.attr("class", "legend-item")
169+
.html(`<span class="legend-color" style="background:${teamColors[team]}"></span>${team}`);
170+
});
171+
}
172+
173+
// --- Draw summary table with team badges and colors ---
174+
function drawSummaryTable(teamData, teamColors) {
175+
const summary = [];
176+
Object.keys(teamData).forEach(team => {
177+
const lastRace = teamData[team][teamData[team].length-1];
178+
summary.push({ team, points: lastRace ? lastRace.points : 0 });
179+
});
180+
summary.sort((a, b) => b.points - a.points);
181+
summary.forEach((d, i) => d.position = i + 1);
182+
183+
let tableHTML = `<table>
184+
<tr><th>Pos</th><th>Team</th><th>Points</th></tr>`;
185+
summary.forEach(row => {
186+
tableHTML += `<tr>
187+
<td>${row.position}</td>
188+
<td><span class="team-badge" style="background:${teamColors[row.team]}"></span> ${row.team}</td>
189+
<td>${row.points}</td>
190+
</tr>`;
191+
});
192+
tableHTML += `</table>`;
193+
d3.select("#summaryTable").html(tableHTML);
194+
}
195+
196+
// --- Draw the D3.js line chart for team points evolution ---
197+
function drawLineChart(teamData, races, season, teamColors) {
198+
d3.select("#chart").html(""); // reset
199+
200+
const margin = { top: 28, right: 24, bottom: 40, left: 58 },
201+
width = 820 - margin.left - margin.right,
202+
height = 380 - margin.top - margin.bottom;
203+
204+
const svg = d3.select("#chart")
205+
.append("svg")
206+
.attr("width", width + margin.left + margin.right)
207+
.attr("height", height + margin.top + margin.bottom)
208+
.append("g")
209+
.attr("transform", `translate(${margin.left},${margin.top})`);
210+
211+
// X axis: race round number
212+
const x = d3.scaleLinear()
213+
.domain([1, races.length])
214+
.range([0, width]);
215+
216+
// Y axis: max cumulative points
217+
const maxPoints = d3.max(Object.values(teamData).flat(), d => d.points);
218+
const y = d3.scaleLinear()
219+
.domain([0, maxPoints*1.08])
220+
.range([height, 0]);
221+
222+
svg.append("g")
223+
.attr("transform", `translate(0,${height})`)
224+
.call(d3.axisBottom(x).ticks(races.length).tickFormat(d => "R" + d))
225+
.selectAll("text").attr("font-size", "12px");
226+
227+
svg.append("g").call(d3.axisLeft(y).ticks(8));
228+
229+
// Chart title
230+
svg.append("text")
231+
.attr("x", width/2)
232+
.attr("y", -10)
233+
.attr("text-anchor", "middle")
234+
.style("font-size", "1.5rem")
235+
.style("fill", "#ffda00")
236+
.text(`Constructor Standings • Season ${season}`);
237+
238+
// Tooltip for points
239+
const tooltip = d3.select("body")
240+
.append("div")
241+
.attr("class", "chart-tooltip")
242+
.style("opacity", 0);
243+
244+
// Draw lines and circles for each team
245+
Object.keys(teamData).forEach(team => {
246+
const color = teamColors[team];
247+
248+
const line = d3.line()
249+
.x((d, i) => x(d.round))
250+
.y(d => y(d.points))
251+
.curve(d3.curveMonotoneX);
252+
253+
svg.append("path")
254+
.datum(teamData[team])
255+
.attr("fill", "none")
256+
.attr("stroke", color)
257+
.attr("stroke-width", 3)
258+
.attr("opacity", 0.88)
259+
.attr("d", line);
260+
261+
svg.selectAll(`.dot-${team.replace(/\s/g, "")}`)
262+
.data(teamData[team])
263+
.enter()
264+
.append("circle")
265+
.attr("class", `dot dot-${team.replace(/\s/g, "")}`)
266+
.attr("cx", d => x(d.round))
267+
.attr("cy", d => y(d.points))
268+
.attr("r", 5)
269+
.attr("fill", color)
270+
.on("mouseover", function(event, d) {
271+
tooltip.transition().duration(120).style("opacity", 1);
272+
tooltip.html(`<strong>${team}</strong><br>Round: ${d.round}<br>Points: ${d.points}`)
273+
.style("left", (event.pageX + 15) + "px")
274+
.style("top", (event.pageY - 20) + "px");
275+
})
276+
.on("mouseout", function() {
277+
tooltip.transition().duration(200).style("opacity", 0);
278+
});
279+
});
280+
}

0 commit comments

Comments
 (0)