From 45d611c9d048373a1ee6d392079eca49a911971d Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Sun, 4 Aug 2024 21:16:58 -0400 Subject: [PATCH 1/5] Basic prediction page is done --- components/stats/PredictionScreen.tsx | 97 +++++++++++++++++++ lib/Types.ts | 2 +- .../[seasonSlug]/[competitonSlug]/stats.tsx | 19 ++-- 3 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 components/stats/PredictionScreen.tsx diff --git a/components/stats/PredictionScreen.tsx b/components/stats/PredictionScreen.tsx new file mode 100644 index 00000000..aca2da1b --- /dev/null +++ b/components/stats/PredictionScreen.tsx @@ -0,0 +1,97 @@ +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 ( +
+

{props.name} Alliance

+
+ {props.alliance.map((team, index) => + + )} +
+
+ ); +} + +export default function PredictionScreen(props: { reports: Report[], teams: number[], game: Game }) { + const [reportsByTeam, setReportsByTeam] = useState>({}); + + // 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); + + 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 avgPointsBlueAllianceTotal = avgPointsBlueAllianceIndividual.reduce((acc, points) => acc + points, 0); + const avgPointsRedAllianceTotal = avgPointsRedAllianceIndividual.reduce((acc, points) => acc + points, 0); + + console.log(blueAlliance, blueAllianceFilled, avgPointsBlueAllianceIndividual); + + return ( +
+
+ updateAlliance(setBlueAlliance, blueAlliance, index, team)} name="Blue" color="blue-500" /> + updateAlliance(setRedAlliance, redAlliance, index, team)} name="Red" color="red-500" /> +
+
+ team!.toString()).concat(["Blue Alliance", "Red Alliance"]), + }} + options={{ + responsive: true, + maintainAspectRatio: false, + }} + height={"450px"} + /> +
+
+ ); +} \ No newline at end of file diff --git a/lib/Types.ts b/lib/Types.ts index bb91852c..2867e2d4 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -131,7 +131,7 @@ export enum League { FTC = "FTC", FRC = "FRC" } -export class Game { +export class Game { name: string; year: number; diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx index 62f27201..fc84de9a 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx @@ -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"); @@ -109,18 +111,18 @@ export default function Stats(props: StatsPageProps) { setPage(1); }} > - Picklist (Beta) + Picklist { - // setPage(2); + setPage(2); }} > - Prediction (Coming Soon!) + Prediction {/* Resync {" "} @@ -137,11 +139,14 @@ export default function Stats(props: StatsPageProps) { )} - {page === 1 - ? - : <> + } + { + page === 2 && + } ); From 94458194b61e23a4401e32286bbd2af8a71cbfd1 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Mon, 5 Aug 2024 20:42:10 -0400 Subject: [PATCH 2/5] Stacked charts --- components/stats/PredictionScreen.tsx | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/components/stats/PredictionScreen.tsx b/components/stats/PredictionScreen.tsx index aca2da1b..da7692e1 100644 --- a/components/stats/PredictionScreen.tsx +++ b/components/stats/PredictionScreen.tsx @@ -56,10 +56,17 @@ export default function PredictionScreen(props: { reports: Report[], teams: numb const avgPointsBlueAllianceIndividual = blueAllianceFilled.map((team) => props.game.getAvgPoints(reportsByTeam[team!])); const avgPointsRedAllianceIndividual = redAllianceFilled.map((team) => props.game.getAvgPoints(reportsByTeam[team!])); - const avgPointsBlueAllianceTotal = avgPointsBlueAllianceIndividual.reduce((acc, points) => acc + points, 0); - const avgPointsRedAllianceTotal = 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; - console.log(blueAlliance, blueAllianceFilled, avgPointsBlueAllianceIndividual); + 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)`], + }); + } return (
@@ -72,22 +79,20 @@ export default function PredictionScreen(props: { reports: Report[], teams: numb
team!.toString()).concat(["Blue Alliance", "Red Alliance"]), + datasets, + labels: ["Blue Alliance", "Red Alliance"], }} options={{ responsive: true, maintainAspectRatio: false, + scales: { + x: { + stacked: true + }, + y: { + stacked: true + } + } }} height={"450px"} /> From f8c9286e22193cf6882819e61ba5b16b75ee7bea Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Mon, 5 Aug 2024 20:51:53 -0400 Subject: [PATCH 3/5] Winner text --- components/stats/PredictionScreen.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/components/stats/PredictionScreen.tsx b/components/stats/PredictionScreen.tsx index da7692e1..457db49d 100644 --- a/components/stats/PredictionScreen.tsx +++ b/components/stats/PredictionScreen.tsx @@ -56,6 +56,9 @@ export default function PredictionScreen(props: { reports: Report[], teams: numb 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; @@ -68,6 +71,17 @@ export default function PredictionScreen(props: { reports: Report[], teams: numb }); } + 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 (
@@ -76,6 +90,9 @@ export default function PredictionScreen(props: { reports: Report[], teams: numb updateAlliance(setRedAlliance, redAlliance, index, team)} name="Red" color="red-500" />
+

+ {pointDiff != 0 ? `${winner} wins by ${Math.abs(pointDiff)} points` : winner} ({totalPointsBlueAlliance} - {totalPointsRedAlliance}) +

Date: Mon, 5 Aug 2024 20:54:31 -0400 Subject: [PATCH 4/5] Note about the infinitely expanding Bar graph --- components/stats/PredictionScreen.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/stats/PredictionScreen.tsx b/components/stats/PredictionScreen.tsx index 457db49d..4958eee1 100644 --- a/components/stats/PredictionScreen.tsx +++ b/components/stats/PredictionScreen.tsx @@ -93,7 +93,8 @@ export default function PredictionScreen(props: { reports: Report[], teams: numb

{pointDiff != 0 ? `${winner} wins by ${Math.abs(pointDiff)} points` : winner} ({totalPointsBlueAlliance} - {totalPointsRedAlliance})

-
+ {/* If we don't have bar graph in a div, it will vertically expand into infinity. Don't question it. - Renato */ } +
Date: Mon, 5 Aug 2024 20:56:35 -0400 Subject: [PATCH 5/5] Added key prop to AllianceBuilder's select elements --- components/stats/PredictionScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/stats/PredictionScreen.tsx b/components/stats/PredictionScreen.tsx index 4958eee1..42309467 100644 --- a/components/stats/PredictionScreen.tsx +++ b/components/stats/PredictionScreen.tsx @@ -14,7 +14,7 @@ function AllianceBuilder(props: {

{props.name} Alliance

{props.alliance.map((team, index) => - props.update(index, parseInt(e.target.value))}> {props.teams.map((t) => )}