Compare commits
2 Commits
978aafc204
...
d6e5d0334c
Author | SHA1 | Date | |
---|---|---|---|
d6e5d0334c | |||
5fef47f692 |
26
analysis.py
26
analysis.py
@ -6,8 +6,9 @@ from fastapi.responses import JSONResponse
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from sqlmodel import Session, func, select
|
from sqlmodel import Session, func, select
|
||||||
from sqlmodel.sql.expression import SelectOfScalar
|
from sqlmodel.sql.expression import SelectOfScalar
|
||||||
from db import Chemistry, Player, engine
|
from db import Chemistry, MVPRanking, Player, engine
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
|
import numpy as np
|
||||||
import matplotlib
|
import matplotlib
|
||||||
|
|
||||||
matplotlib.use("agg")
|
matplotlib.use("agg")
|
||||||
@ -18,6 +19,7 @@ analysis_router = APIRouter(prefix="/analysis")
|
|||||||
|
|
||||||
|
|
||||||
C = Chemistry
|
C = Chemistry
|
||||||
|
R = MVPRanking
|
||||||
P = Player
|
P = Player
|
||||||
|
|
||||||
|
|
||||||
@ -192,9 +194,31 @@ async def render_sociogram(params: Params):
|
|||||||
return {"image": encoded_image}
|
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("/json", endpoint=sociogram_json, methods=["GET"])
|
||||||
analysis_router.add_api_route("/graph_json", endpoint=graph_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("/image", endpoint=render_sociogram, methods=["POST"])
|
||||||
|
analysis_router.add_api_route("/mvp", endpoint=mvp, methods=["GET"])
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
|
@ -10,16 +10,16 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-sortablejs": "^6.1.4",
|
"react-sortablejs": "^6.1.4",
|
||||||
"reagraph": "^4.21.2",
|
"reagraph": "^4.21.2",
|
||||||
"sortablejs": "^1.15.6"
|
"sortablejs": "^1.15.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "18.3.18",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "18.3.5",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.17.0",
|
"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() {
|
export default function Analysis() {
|
||||||
const [image, setImage] = useState("");
|
const [image, setImage] = useState("");
|
||||||
const [params, setParams] = useState<Params>({
|
const [params, setParams] = useState<Params>({
|
||||||
|
10
src/App.tsx
10
src/App.tsx
@ -6,6 +6,7 @@ import Rankings from "./Rankings";
|
|||||||
import { BrowserRouter, Routes, Route } from "react-router";
|
import { BrowserRouter, Routes, Route } from "react-router";
|
||||||
import { SessionProvider } from "./Session";
|
import { SessionProvider } from "./Session";
|
||||||
import { GraphComponent } from "./Network";
|
import { GraphComponent } from "./Network";
|
||||||
|
import MVPChart from "./MVPChart";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -13,16 +14,25 @@ function App() {
|
|||||||
<Header />
|
<Header />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<Rankings />} />
|
<Route index element={<Rankings />} />
|
||||||
|
|
||||||
<Route path="/network" element={
|
<Route path="/network" element={
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<GraphComponent />
|
<GraphComponent />
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/analysis" element={
|
<Route path="/analysis" element={
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<Analysis />
|
<Analysis />
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
|
<Route path="/mvp" element={
|
||||||
|
<SessionProvider>
|
||||||
|
<MVPChart />
|
||||||
|
</SessionProvider>
|
||||||
|
} />
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
<Footer />
|
<Footer />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
29
src/MVPChart.tsx
Normal file
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
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;
|
Loading…
x
Reference in New Issue
Block a user