Skip to content

Commit

Permalink
Merge pull request #215 from Decatur-Robotics/prediction-screen-on-st…
Browse files Browse the repository at this point in the history
…ats-page
  • Loading branch information
renatodellosso authored Aug 10, 2024
2 parents 1a7e820 + 28354e1 commit e61d911
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 8 deletions.
120 changes: 120 additions & 0 deletions components/stats/PredictionScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Game, Report } from "@/lib/Types";
import { useEffect, useState } from "react";
import { Bar } from "react-chartjs-2";

function AllianceBuilder(props: {
teams: number[],
alliance: (number | undefined)[],
update: (index: number, team: number) => void,
name: string,
color: string
}) {
return (
<div className="flex flex-col w-1/2">
<h1 className={`text-${props.color} text-center`}>{props.name} Alliance</h1>
<div className="flex flex-row items-stretch">
{props.alliance.map((team, index) =>
<select key={index} className="w-full select" value={team} onChange={(e) => props.update(index, parseInt(e.target.value))}>
<option value={undefined}>Empty</option>
{props.teams.map((t) => <option key={t} value={t}>{t}</option>)}
</select>
)}
</div>
</div>
);
}

export default function PredictionScreen(props: { reports: Report[], teams: number[], game: Game }) {
const [reportsByTeam, setReportsByTeam] = useState<Record<number, Report[]>>({});

// Array(length) constructor doesn't actually fill the array with undefined, so we have to do it manually
const [blueAlliance, setBlueAlliance] = useState<(number | undefined)[]>(Array(props.game.allianceSize).fill(undefined));
const [redAlliance, setRedAlliance] = useState<(number | undefined)[]>(Array(props.game.allianceSize).fill(undefined));

useEffect(() => {
const reportsByTeam = props.reports.reduce((acc, report) => {
if (!acc[report.robotNumber]) {
acc[report.robotNumber] = [];
}

acc[report.robotNumber].push(report);
return acc;
}, {} as Record<number, Report[]>);

setReportsByTeam(reportsByTeam);
}, [props.reports]);

function updateAlliance(setAlliance: (alliance: (number | undefined)[]) => void, alliance: (number | undefined)[],
index: number, team: number) {
alliance[index] = team;
setAlliance([...alliance]); // We have to create a new array for the update to work
}

const blueAllianceFilled = blueAlliance.filter((team) => team !== undefined);
const redAllianceFilled = redAlliance.filter((team) => team !== undefined);

const avgPointsBlueAllianceIndividual = blueAllianceFilled.map((team) => props.game.getAvgPoints(reportsByTeam[team!]));
const avgPointsRedAllianceIndividual = redAllianceFilled.map((team) => props.game.getAvgPoints(reportsByTeam[team!]));

const totalPointsBlueAlliance = avgPointsBlueAllianceIndividual.reduce((acc, points) => acc + points, 0);
const totalPointsRedAlliance = avgPointsRedAllianceIndividual.reduce((acc, points) => acc + points, 0);

const datasets = [];
for (let i = 0; i < Math.max(avgPointsBlueAllianceIndividual.length, avgPointsRedAllianceIndividual.length); i++) {
const blue = avgPointsBlueAllianceIndividual[i] || undefined;
const red = avgPointsRedAllianceIndividual[i] || undefined;

datasets.push({
data: [blue, red],
label: `Team ${blueAllianceFilled[i] || "Empty"} vs Team ${redAllianceFilled[i] || "Empty"}`,
backgroundColor: [`rgba(0, ${i * 255 / 2}, 235, 1)`, `rgba(235, ${i * 255 / 2}, 0, 1)`],
});
}

const pointDiff = totalPointsBlueAlliance - totalPointsRedAlliance;
let winner = "Tie";
let color = "";
if (pointDiff > 0) {
winner = "Blue Alliance";
color = "blue-500";
} else if (pointDiff < 0) {
winner = "Red Alliance";
color = "red-500";
}

return (
<div className="w-full h-fit flex flex-col space-y-2">
<div className="flex flex-row w-full">
<AllianceBuilder teams={props.teams} alliance={blueAlliance}
update={(index, team) => updateAlliance(setBlueAlliance, blueAlliance, index, team)} name="Blue" color="blue-500" />
<AllianceBuilder teams={props.teams} alliance={redAlliance}
update={(index, team) => updateAlliance(setRedAlliance, redAlliance, index, team)} name="Red" color="red-500" />
</div>
<h1 className={`text-xl text-center text-${color}`}>
{pointDiff != 0 ? `${winner} wins by ${Math.abs(pointDiff)} points` : winner} ({totalPointsBlueAlliance} - {totalPointsRedAlliance})
</h1>
{/* If we don't have bar graph in a div, it will vertically expand into infinity. Don't question it. - Renato */ }
<div>
<Bar
data={{
datasets,
labels: ["Blue Alliance", "Red Alliance"],
}}
options={{
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true
},
y: {
stacked: true
}
}
}}
height={"450px"}
/>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion lib/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export enum League {
FTC = "FTC", FRC = "FRC"
}

export class Game<TQuantData extends QuantData, TPitData extends PitReportData> {
export class Game<TQuantData extends QuantData = QuantData, TPitData extends PitReportData = PitReportData> {
name: string;

year: number;
Expand Down
19 changes: 12 additions & 7 deletions pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import ClientAPI from "@/lib/client/ClientAPI";
import { team } from "slack";
import { NotLinkedToTba } from "@/lib/client/ClientUtils";
import { defaultGameId } from "@/lib/client/GameId";
import PredictionScreen from "@/components/stats/PredictionScreen";
import { games } from "@/lib/games";

const api = new ClientAPI("gearboxiscool");

Expand Down Expand Up @@ -110,18 +112,18 @@ export default function Stats(props: StatsPageProps) {
setPage(1);
}}
>
Picklist (Beta)
Picklist
</a>
<a
role="tab"
className={`tab tab-disabled tab-md ${
className={`tab tab-md ${
page === 2 ? "tab-active" : ""
}`}
onClick={() => {
// setPage(2);
setPage(2);
}}
>
Prediction (Coming Soon!)
Prediction
</a>
{/* <a role="tab" className={`tab tab-md `} onClick={resync}>
Resync {" "}
Expand All @@ -138,11 +140,14 @@ export default function Stats(props: StatsPageProps) {
<TeamPage reports={reports} pitReports={pitReports} subjectiveReports={subjectiveReports}
gameId={props.competition?.gameId ?? defaultGameId} />
)}
{page === 1
? <PicklistScreen
{page === 1 &&
<PicklistScreen
teams={Array.from(teams)} reports={reports} expectedTeamCount={props.competition.pitReports.length}
picklistId={props.competition.picklist} />
: <></>
}
{
page === 2 &&
<PredictionScreen reports={reports} teams={Array.from(teams)} game={games[props.competition.gameId]} />
}
</Container>
);
Expand Down

0 comments on commit e61d911

Please sign in to comment.