Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@

import "./globals.css";
import { ThemeProvider } from "../components/ui/theme-provider";
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/structure/app-sidebar";
import { ModeToggle } from "@/components/ui/ModeToggle";
import { Sheet, SheetTrigger, SheetContent, SheetTitle, SheetDescription } from "@/components/ui/sheet";
import { Menu } from "lucide-react";

export default function RootLayout({
children,
Expand All @@ -23,7 +26,25 @@ export default function RootLayout({
} as React.CSSProperties & Record<string, string>
}
>
<AppSidebar />
{/* Mobile Burger Button and Sheet */}
<Sheet>
<SheetTrigger asChild>
<button className="md:hidden p-2 fixed top-4 left-4 z-50 rounded shadow">
<Menu className="h-7 w-7" />
</button>
</SheetTrigger>
<SheetContent side="left" className="p-0">
<SheetTitle className="sr-only">Sidebar Menu</SheetTitle>
<SheetDescription className="sr-only">Main navigation and links</SheetDescription>
<AppSidebar isMobile />
</SheetContent>
</Sheet>

{/* Desktop Sidebar */}
<div className="hidden md:block">
<AppSidebar />
</div>

<SidebarInset className="relative p-4 overflow-y-auto">
<div className="fixed top-4 right-4 z-50">
<ModeToggle />
Expand Down
22 changes: 10 additions & 12 deletions frontend/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ export default function Home() {
return (
<div className="p-4 pl-8">
<ModeToggle />
<h1 className="text-5xl font-extrabold text-center mb-2 text-primary">Formula 1 DataLab</h1>
<div className="text-center mb-4 max-w-3xl mx-auto">
<p className="text-xl text-muted-foreground mb-2">
The primary goal of this site is to provide a user-friendly platform for quick, interactive exploration and visualization of historical Formula 1 data across various categories.
</p>
<p className="text-xl text-muted-foreground mb-2">
Website is work in progress, and will be updated with more features and data visualizations.
</p>
<p className="text-lg text-primary-foreground/70">Start your exploration with the key statistics below, or dive deeper into specific categories.</p>
</div>
<h1 className="text-5xl font-extrabold text-center mb-2 bg-gradient-to-r from-orange-800 to-yellow-300 bg-clip-text text-transparent drop-shadow-2xl">
Formula 1 DataLab
</h1>
<div className="text-center mb-4 max-w-6xl mx-auto">
<p className="text-xl text-gray-600 dark:text-gray-400 font-medium mb-2 opacity-80">
Website is work in progress, and will be updated with more features in future. The goal is to provide a user-friendly platform for quick, interactive exploration and visualization of historical Formula 1 data across different racing aspects.
</p>
</div>
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-12">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Card className="flex flex-col items-center justify-center p-4 text-center">
Expand Down Expand Up @@ -80,10 +78,10 @@ export default function Home() {
<div className="flex items-center justify-between p-3 rounded-md border">
<div className="flex items-center gap-2">
<Crown className="h-5 w-5 text-amber-500" />
<p className="text-lg font-semibold">Most Championships:</p>
<p className="text-lg font-semibold">Most Champs:</p>
</div>

<Badge variant="outline" className="text-base px-3 py-1 bg-amber-500 text-black">L. Hamilton (7)</Badge>
<Badge variant="outline" className="text-base px-3 py-1 bg-amber-500 text-black">Hamilton/M.Scum. (7)</Badge>
</div>
<div className="flex items-center justify-between p-3 rounded-md border">
<div className="flex items-center gap-2">
Expand Down
156 changes: 156 additions & 0 deletions frontend/app/races/sandbox/scatter_chart_with_cells_lap_times.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// https://recharts.org/en-US/examples/ScatterChartWithCells

import React, { PureComponent } from 'react';
import {
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell, // <--- Make sure Cell is imported here!
// Legend // No longer strictly needed as colors are directly on points, but could be used for custom legends
} from 'recharts';

// Helper function to format milliseconds into a human-readable time (MM:SS.mmm)
const formatMillisecondsToTime = (milliseconds) => {
if (milliseconds === null || isNaN(milliseconds)) return "N/A";
const totalSeconds = milliseconds / 1000;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes.toFixed(0).padStart(2, '0')}:${seconds.toFixed(3).padStart(6, '0')}`;
};

// Function to generate synthetic lap times for a driver
const generateLapTimes = (startLap, endLap, baseTimeMs, variationMs, slowLapInterval = -1) => {
const lapTimes = [];
for (let lap = startLap; lap <= endLap; lap++) {
let time = baseTimeMs + (Math.random() - 0.5) * variationMs * 2; // Random variation

// Simulate occasional slower laps (e.g., traffic, tire deg) - kept this, but no pit stops
if (slowLapInterval > 0 && lap % slowLapInterval === 0 && lap > startLap) {
time += Math.random() * 800 + 300; // Add 0.3-1.1 seconds for a "slower lap"
}
lapTimes.push({ lap, time: Math.round(time) });
}
return lapTimes;
};


// Hardcoded data for 5 drivers' lap times (60 laps each)
const driverLapDataRaw = [
{
name: "Max Verstappen",
color: "#0600EF", // Red Bull Blue
times: generateLapTimes(1, 60, 81000, 300, 10), // Base 1:21.000, +/- 0.3s, slightly slower every 10 laps
},
{
name: "Charles Leclerc",
color: "#E8002D", // Ferrari Red
times: generateLapTimes(1, 60, 81500, 350, 15), // Base 1:21.500, +/- 0.35s, slightly slower every 15 laps
},
{
name: "Lewis Hamilton",
color: "#00D2BE", // Mercedes Teal
times: generateLapTimes(1, 60, 81700, 400, 8), // Base 1:21.700, +/- 0.4s, slightly slower every 8 laps
},
{
name: "Sergio Perez",
color: "#4682B4", // SteelBlue - a distinct shade for Perez from Max
times: generateLapTimes(1, 60, 82200, 450, 12), // Base 1:22.200, +/- 0.45s, slightly slower every 12 laps
},
{
name: "Lando Norris",
color: "#FF8700", // McLaren Orange
times: generateLapTimes(1, 60, 82000, 380, 7), // Base 1:22.000, +/- 0.38s, slightly slower every 7 laps
},
];

// IMPORTANT: Transform the data structure for the chart layout
const combinedLapTimesForDriversXAxis = driverLapDataRaw.flatMap(driver =>
driver.times.map(lapInfo => ({
driverName: driver.name,
time: lapInfo.time,
lap: lapInfo.lap, // Keep lap info for tooltip
color: driver.color // Pass the driver's specific color
}))
);

export default class Example extends PureComponent {
static demoUrl = 'https://codesandbox.io/p/sandbox/scatter-chart-with-double-yaxes-3yzqtm';

render() {
// Determine the min/max lap times (milliseconds) for the Y-axis
const allTimes = combinedLapTimesForDriversXAxis.map(d => d.time);
const minTime = Math.min(...allTimes);
const maxTime = Math.max(...allTimes);

// Set a "zoomed in" domain for the Y-axis now that pit stops are removed.
// We can make the zoom much tighter.
const yDomainStart = minTime - 300; // Small padding below the lowest lap time
const yDomainEnd = maxTime + 300; // Small padding above the highest lap time


return (
<div style={{ width: '100%', height: 500 }}>
<ResponsiveContainer width="100%" height="100%">
<ScatterChart
margin={{
top: 20,
right: 20,
bottom: 20,
left: 20,
}}
>
<CartesianGrid strokeDasharray="3 3" />
{/* X-Axis: Drivers (Categorical) */}
<XAxis
type="category"
dataKey="driverName"
name="Driver"
tickLine={false}
padding={{ left: 20, right: 20 }}
/>
{/* Y-Axis: Lap Time (Numerical) */}
<YAxis
type="number"
dataKey="time"
name="Lap Time"
unit=""
tickFormatter={formatMillisecondsToTime}
domain={[yDomainStart, yDomainEnd]}
/>
<Tooltip
cursor={{ strokeDasharray: '3 3' }}
formatter={(value, name, props) => {
if (name === 'time') {
return [`Time: ${formatMillisecondsToTime(value)}`, `Lap ${props.payload.lap}`];
}
return [value, name];
}}
labelFormatter={(label) => `Driver: ${label}`}
/>
{/* --- FIX BASED ON YOUR REFERENCE EXAMPLE --- */}
<Scatter
name="Lap Times"
data={combinedLapTimesForDriversXAxis} // Data array goes here
// *** Removed fill, shape, and dot from Scatter as Cell will handle it ***
>
{/* Map over the data array to render a Cell for each data point */}
{combinedLapTimesForDriversXAxis.map((entry, index) => (
<Cell
key={`cell-${entry.driverName}-${index}`} // Unique key for each cell
fill={entry.color} // Use the color from our data object
// You can also add other SVG properties here if needed, like r for radius
// r={2} // Example: Set radius if needed, or let Recharts default
/>
))}
</Scatter>
{/* --- END FIX --- */}
</ScatterChart>
</ResponsiveContainer>
</div>
);
}
}
90 changes: 90 additions & 0 deletions frontend/app/races/sandbox/stacked_bar_chart_pitstop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { PureComponent } from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';

// Helper function to format milliseconds to a human-readable time (seconds with decimals)
const formatMillisecondsToSeconds = (milliseconds) => {
if (milliseconds === null || isNaN(milliseconds)) return "N/A";
return (milliseconds / 1000).toFixed(2); // Convert to seconds, 2 decimal places
};

// Hardcoded data for pit stops for 5 drivers (assuming 3 pit stops each)
const driverPitStopData = [
{
driverId: "830",
pitStop1: 2200,
pitStop3: 2300,
},
{
driverId: "21",
pitStop1: 2400,
pitStop2: 2600,
pitStop3: 2500,
},
{
driver: "Lewis Hamilton",
pitStop1: 2300,
pitStop2: 2700,
pitStop3: 2400,
},
{
driver: "Sergio Perez",
pitStop1: 2500,
pitStop2: 2800,
pitStop3: 2600,
},
{
driver: "Lando Norris",
pitStop1: 2100,
pitStop2: 2400,
pitStop3: 2200,
},
];

export default class PitStopChart extends PureComponent {
static demoUrl = 'https://codesandbox.io/p/sandbox/stacked-bar-chart-7fwfgj'; // Reference URL

render() {
return (
<div style={{ width: '100%', height: 400 }}> {/* Set a height for the container */}
<ResponsiveContainer width="100%" height="100%">
<BarChart
width={500}
height={300}
data={driverPitStopData} // Use our hardcoded pit stop data
margin={{
top: 20,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="driver" /> {/* X-axis displays driver names */}
<YAxis
tickFormatter={formatMillisecondsToSeconds} // Format Y-axis ticks to seconds
label={{ value: 'Total Pit Stop Time (s)', angle: -90, position: 'insideLeft' }} // Label for Y-axis
/>
<Tooltip
formatter={(value, name, props) => {
// Custom formatter for tooltip to show time in seconds and rename dataKey
if (name.startsWith('pitStop')) {
const pitStopNumber = name.replace('pitStop', 'Pit Stop ');
return [`${formatMillisecondsToSeconds(value)} s`, pitStopNumber];
}
return [value, name];
}}
// Optional: Custom label formatter for tooltip
labelFormatter={(label) => `Driver: ${label}`}
/>
<Legend />
{/* Stacked Bars for each pit stop, with different colors */}
<Bar dataKey="pitStop1" stackId="totalPitTime" fill="#FF8700" name="Pit Stop 1" /> {/* McLaren Orange for 1st stop */}
<Bar dataKey="pitStop2" stackId="totalPitTime" fill="#00D2BE" name="Pit Stop 2" /> {/* Mercedes Teal for 2nd stop */}
<Bar dataKey="pitStop3" stackId="totalPitTime" fill="#E8002D" name="Pit Stop 3" /> {/* Ferrari Red for 3rd stop */}
{/* Add more Bar components here if drivers have more pit stops */}
</BarChart>
</ResponsiveContainer>
</div>
);
}
}
Loading