From 638e8bf20cd916897325ebbdcff9e25e98c854b5 Mon Sep 17 00:00:00 2001 From: julius Date: Mon, 26 May 2025 07:33:31 +0200 Subject: [PATCH] feat: gender-separated `MVPChart` in mixed teams --- cutt/analysis.py | 72 ++++++++++++++++++++----------- cutt/demo.py | 2 +- src/MVPChart.tsx | 21 ++++++--- src/RaceChart.tsx | 108 ++++++++++++++++++++++++---------------------- src/types.ts | 2 +- 5 files changed, 121 insertions(+), 84 deletions(-) diff --git a/cutt/analysis.py b/cutt/analysis.py index cfb1601..e034cbc 100644 --- a/cutt/analysis.py +++ b/cutt/analysis.py @@ -349,24 +349,26 @@ def last_submissions( def mvp( - request: Annotated[TeamScopedRequest, Security(verify_team_scope)], + request: Annotated[TeamScopedRequest, Security(verify_team_scope)], mixed=False ): - ranks = dict() if request.team_id == 42: + ranks = {} random.seed(42) players = [request.user] + demo_players for p in players: random.shuffle(players) for i, p in enumerate(players): - ranks[p.display_name] = ranks.get(p.display_name, []) + [i + 1] + ranks[p.id] = ranks.get(p.id, []) + [i + 1] return [ - { - "name": p, - "rank": f"{np.mean(v):.02f}", - "std": f"{np.std(v):.02f}", - "n": len(v), - } - for p, v in ranks.items() + [ + { + "p_id": p_id, + "rank": f"{np.mean(v):.02f}", + "std": f"{np.std(v):.02f}", + "n": len(v), + } + for i, (p_id, v) in enumerate(ranks.items()) + ] ] with Session(engine) as session: @@ -378,7 +380,7 @@ def mvp( ).all() if not players: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - player_map = {p.id: p.display_name for p in players} + player_map = {p.id: p for p in players} subquery = ( select(R.user, func.max(R.time).label("latest")) .where(R.team == request.team_id) @@ -388,25 +390,45 @@ def mvp( statement2 = select(R).join( subquery, (R.user == subquery.c.user) & (R.time == subquery.c.latest) ) - for r in session.exec(statement2): - for i, p_id in enumerate(r.mvps): - if p_id not in player_map: - continue - p = player_map[p_id] - ranks[p] = ranks.get(p, []) + [i + 1] + if mixed: + all_ranks = [] + for gender in ["fmp", "mmp"]: + ranks = {} + for r in session.exec(statement2): + mvps = [ + p_id + for p_id in r.mvps + if p_id in player_map and player_map[p_id].gender == gender + ] + for i, p_id in enumerate(mvps): + p = player_map[p_id] + ranks[p_id] = ranks.get(p_id, []) + [i + 1] + all_ranks.append(ranks) + else: + ranks = {} + for r in session.exec(statement2): + for i, p_id in enumerate(r.mvps): + if p_id not in player_map: + continue + p = player_map[p_id] + ranks[p_id] = ranks.get(p_id, []) + [i + 1] + all_ranks = [ranks] - if not ranks: + if not all_ranks: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="no entries found" ) return [ - { - "name": p, - "rank": f"{np.mean(v):.02f}", - "std": f"{np.std(v):.02f}", - "n": len(v), - } - for p, v in ranks.items() + [ + { + "p_id": p_id, + "rank": f"{np.mean(v):.02f}", + "std": f"{np.std(v):.02f}", + "n": len(v), + } + for p_id, v in ranks.items() + ] + for ranks in all_ranks ] diff --git a/cutt/demo.py b/cutt/demo.py index 4224023..14d17f4 100644 --- a/cutt/demo.py +++ b/cutt/demo.py @@ -16,7 +16,7 @@ names = [ demo_players = [ Player.model_validate( { - "id": i, + "id": i + 4200, "display_name": name, "username": name.lower().replace(" ", "").replace(".", ""), "gender": gender, diff --git a/src/MVPChart.tsx b/src/MVPChart.tsx index 085e91e..5257f8a 100644 --- a/src/MVPChart.tsx +++ b/src/MVPChart.tsx @@ -6,12 +6,13 @@ import { useSession } from "./Session"; import { useNavigate } from "react-router"; const MVPChart = () => { - let initialData = {} as PlayerRanking[]; + let initialData = {} as PlayerRanking[][]; const [data, setData] = useState(initialData); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [showStd, setShowStd] = useState(false); const { user, teams } = useSession(); + const [mixed, setMixed] = useState(false); const navigate = useNavigate(); useEffect(() => { user?.scopes.includes(`team:${teams?.activeTeam}`) || @@ -19,21 +20,30 @@ const MVPChart = () => { navigate("/", { replace: true }); }, [user]); + useEffect(() => { + if (teams) { + const activeTeam = teams.teams.find( + (team) => team.id == teams.activeTeam + ); + activeTeam && setMixed(activeTeam.mixed); + } + }, [teams]); + async function loadData() { setLoading(true); if (teams) { - await apiAuth(`analysis/mvp/${teams?.activeTeam}`, null) + await apiAuth(`analysis/mvp/${teams?.activeTeam}?mixed=${mixed}`, null) .then((data) => { if (data.detail) { setError(data.detail); return initialData; } else { setError(""); - return data as Promise; + return data as Promise; } }) .then((data) => { - setData(data.sort((a, b) => a.rank - b.rank)); + setData(data.map((_data) => _data.sort((a, b) => a.rank - b.rank))); }) .catch(() => setError("no access")); setLoading(false); @@ -46,7 +56,8 @@ const MVPChart = () => { if (loading) return ; else if (error) return {error}; - else return ; + else + return data.map((_data) => ); }; export default MVPChart; diff --git a/src/RaceChart.tsx b/src/RaceChart.tsx index 2b87515..e7480f9 100644 --- a/src/RaceChart.tsx +++ b/src/RaceChart.tsx @@ -1,8 +1,9 @@ import { FC, useEffect, useState } from "react"; import { PlayerRanking } from "./types"; +import { useSession } from "./Session"; interface RaceChartProps { - players: PlayerRanking[]; + playerRanks: PlayerRanking[]; std: boolean; } @@ -13,15 +14,14 @@ const determineNiceWidth = (width: number) => { else return width * 0.96; }; -const RaceChart: FC = ({ players, std }) => { +const RaceChart: FC = ({ playerRanks, std }) => { const [width, setWidth] = useState(determineNiceWidth(window.innerWidth)); - //const [height, setHeight] = useState(window.innerHeight); - const height = (players.length + 1) * 40; + const height = (playerRanks.length + 1) * 40; + const { players } = useSession(); useEffect(() => { const handleResize = () => { setWidth(determineNiceWidth(window.innerWidth)); - //setHeight(window.innerHeight); }; window.addEventListener("resize", handleResize); return () => { @@ -30,18 +30,18 @@ const RaceChart: FC = ({ players, std }) => { }, []); const padding = 24; const gap = 8; - const maxValue = Math.max(...players.map((player) => player.rank)) + 1; - const barHeight = (height - 2 * padding) / players.length; + const maxValue = Math.max(...playerRanks.map((player) => player.rank)) + 1; + const barHeight = (height - 2 * padding) / playerRanks.length; const fontSize = Math.min(barHeight - 1.5 * gap, width / 22); return ( - {players.map((player, index) => ( + {playerRanks.map((playerRank, index) => ( = ({ players, std }) => { /> ))} - {players.map((player, index) => ( - - - {`${String(index + 1).padStart(2)}. ${player.name}`} - - p.name.length))) * - fontSize * - 0.66 - } - y={index * barHeight + barHeight / 2 + padding + gap / 2} - width={(1 - player.rank / maxValue) * width} - height={barHeight - 8} // subtract 2 for some spacing between bars - fontSize={0.8 * fontSize} - fill="aliceblue" - stroke="#36c" - fontWeight={"bold"} - fontFamily="monospace" - strokeWidth={4} - paintOrder={"stroke fill"} - style={{ whiteSpace: "pre" }} - > - {`${String(player.rank).padStart(5)} ± ${player.std} N = ${player.n}`} - - - ))} + {playerRanks.map((playerRank, index) => { + const player = players!.find((p) => p.id === playerRank.p_id); + return ( + + + {`${String(index + 1).padStart(2)}. ${player?.display_name}`} + + p.display_name.length))) * + fontSize * + 0.66 + } + y={index * barHeight + barHeight / 2 + padding + gap / 2} + width={(1 - playerRank.rank / maxValue) * width} + height={barHeight - 8} // subtract 2 for some spacing between bars + fontSize={0.8 * fontSize} + fill="aliceblue" + stroke="#36c" + fontWeight={"bold"} + fontFamily="monospace" + strokeWidth={4} + paintOrder={"stroke fill"} + style={{ whiteSpace: "pre" }} + > + {`${String(playerRank.rank).padStart(5)} ± ${playerRank.std} N = ${playerRank.n}`} + + + ); + })} ); }; diff --git a/src/types.ts b/src/types.ts index 99fda09..4ab76ae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,7 +13,7 @@ export default interface NetworkData { } export interface PlayerRanking { - name: string; + p_id: number; rank: number; std: number; n: number;