Compare commits

...

2 Commits

Author SHA1 Message Date
d6e5d0334c
feat: add RaceChart 2025-02-24 17:53:01 +01:00
5fef47f692
feat: implement BarChart for MVP 2025-02-24 16:51:50 +01:00
6 changed files with 140 additions and 6 deletions

@ -6,8 +6,9 @@ from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from sqlmodel import Session, func, select
from sqlmodel.sql.expression import SelectOfScalar
from db import Chemistry, Player, engine
from db import Chemistry, MVPRanking, Player, engine
import networkx as nx
import numpy as np
import matplotlib
matplotlib.use("agg")
@ -18,6 +19,7 @@ analysis_router = APIRouter(prefix="/analysis")
C = Chemistry
R = MVPRanking
P = Player
@ -192,9 +194,31 @@ async def render_sociogram(params: Params):
return {"image": encoded_image}
def mvp():
ranks = dict()
with Session(engine) as session:
subquery = (
select(R.user, func.max(R.time).label("latest"))
.where(R.time > datetime(2025, 2, 8))
.group_by(R.user)
.subquery()
)
statement2 = select(R).join(
subquery, (R.user == subquery.c.user) & (R.time == subquery.c.latest)
)
for r in session.exec(statement2):
for i, p in enumerate(r.mvps):
ranks[p] = ranks.get(p, []) + [i + 1]
return [
{"name": p, "rank": f"{np.mean(v):.02f}", "std": f"{np.std(v):.02f}"}
for p, v in ranks.items()
]
analysis_router.add_api_route("/json", endpoint=sociogram_json, methods=["GET"])
analysis_router.add_api_route("/graph_json", endpoint=graph_json, methods=["GET"])
analysis_router.add_api_route("/image", endpoint=render_sociogram, methods=["POST"])
analysis_router.add_api_route("/mvp", endpoint=mvp, methods=["GET"])
if __name__ == "__main__":
with Session(engine) as session:

@ -10,16 +10,16 @@
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-sortablejs": "^6.1.4",
"reagraph": "^4.21.2",
"sortablejs": "^1.15.6"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/sortablejs": "^1.15.8",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0",

@ -42,7 +42,7 @@ interface DeferredProps {
}
let timeoutID: number | null = null;
let timeoutID: NodeJS.Timeout | null = null;
export default function Analysis() {
const [image, setImage] = useState("");
const [params, setParams] = useState<Params>({

@ -6,6 +6,7 @@ import Rankings from "./Rankings";
import { BrowserRouter, Routes, Route } from "react-router";
import { SessionProvider } from "./Session";
import { GraphComponent } from "./Network";
import MVPChart from "./MVPChart";
function App() {
return (
@ -13,16 +14,25 @@ function App() {
<Header />
<Routes>
<Route index element={<Rankings />} />
<Route path="/network" element={
<SessionProvider>
<GraphComponent />
</SessionProvider>
} />
<Route path="/analysis" element={
<SessionProvider>
<Analysis />
</SessionProvider>
} />
<Route path="/mvp" element={
<SessionProvider>
<MVPChart />
</SessionProvider>
} />
</Routes>
<Footer />
</BrowserRouter>

29
src/MVPChart.tsx Normal file

@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
import { apiAuth } from "./api";
import BarChart from "./BarChart";
import { PlayerRanking } from "./types";
import RaceChart from "./RaceChart";
const MVPChart = () => {
const [data, setData] = useState({} as PlayerRanking[]);
const [loading, setLoading] = useState(true);
const [showStd, setShowStd] = useState(false);
async function loadData() {
setLoading(true);
await apiAuth("analysis/mvp", null)
.then(json => json as Promise<PlayerRanking[]>).then(json => { setData(json.sort((a, b) => a.rank - b.rank)) })
setLoading(false);
}
useEffect(() => { loadData() }, [])
return (
<>
{loading ? <span className="loader" /> : <RaceChart std={showStd} players={data} />
}
</>)
}
export default MVPChart;

71
src/RaceChart.tsx Normal file

@ -0,0 +1,71 @@
import { FC, useEffect, useState } from 'react';
import { PlayerRanking } from './types';
interface RaceChartProps {
players: PlayerRanking[];
std: boolean;
}
const determineNiceWidth = (width: number) => {
const max = 1080;
if (width >= max)
return max
else if (width > 768)
return width * 0.8
else
return width * 0.96
}
const RaceChart: FC<RaceChartProps> = ({ players, std }) => {
// State to store window's width and height
const [width, setWidth] = useState(determineNiceWidth(window.innerWidth));
const [height, setHeight] = useState(window.innerHeight);
// Update state on resize
useEffect(() => {
const handleResize = () => {
setWidth(determineNiceWidth(window.innerWidth));
setHeight(window.innerHeight);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const padding = 24;
const gap = 8
const maxValue = Math.max(...players.map((player) => player.rank)) + 1;
const barHeight = (height - 2 * padding) / players.length;
return (
<svg width={width} height={height}>
{players.map((player, index) => (
<rect
key={index}
x={0}
y={index * barHeight + padding}
width={(1 - player.rank / maxValue) * width}
height={barHeight - gap} // subtract 2 for some spacing between bars
fill="#36c"
/>))}
{players.map((player, index) => (
<text
key={index}
x={4}
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="24px"
fill="aliceblue"
stroke='#36c'
strokeWidth={0.8}
fontWeight={"bold"}
>{player.name}</text>
))}
</svg>
)
}
export default RaceChart;