Compare commits
3 Commits
5c76a60df1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
df84c798be
|
|||
|
052065acf9
|
|||
|
039b43be8e
|
@@ -126,6 +126,7 @@ def graph_json(
|
|||||||
return G
|
return G
|
||||||
return JSONResponse({"nodes": nodes, "edges": edges})
|
return JSONResponse({"nodes": nodes, "edges": edges})
|
||||||
|
|
||||||
|
playertypes = playertype(request)
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
players = session.exec(
|
players = session.exec(
|
||||||
select(P)
|
select(P)
|
||||||
@@ -140,7 +141,18 @@ def graph_json(
|
|||||||
)
|
)
|
||||||
for p in players:
|
for p in players:
|
||||||
player_map[p.id] = p.display_name
|
player_map[p.id] = p.display_name
|
||||||
nodes.append({"id": p.display_name, "label": p.display_name})
|
playertype_colour = "#%02x%02x%02x" % (
|
||||||
|
int(playertypes[p.id]["handler"] * 255),
|
||||||
|
int(playertypes[p.id]["combi"] * 255),
|
||||||
|
int(playertypes[p.id]["cutter"] * 255),
|
||||||
|
)
|
||||||
|
nodes.append(
|
||||||
|
{
|
||||||
|
"id": p.display_name,
|
||||||
|
"label": p.display_name,
|
||||||
|
"data": {"playertype": playertype_colour},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
subquery = (
|
subquery = (
|
||||||
select(C.user, func.max(C.time).label("latest"))
|
select(C.user, func.max(C.time).label("latest"))
|
||||||
@@ -207,7 +219,8 @@ def graph_json(
|
|||||||
)
|
)
|
||||||
in_degrees = G.in_degree(weight="weight")
|
in_degrees = G.in_degree(weight="weight")
|
||||||
nodes = [
|
nodes = [
|
||||||
dict(node, **{"data": {"inDegree": in_degrees[node["id"]]}}) for node in nodes
|
dict(node, **{"data": {"inDegree": in_degrees[node["id"]], **node["data"]}})
|
||||||
|
for node in nodes
|
||||||
]
|
]
|
||||||
if networkx_graph:
|
if networkx_graph:
|
||||||
return G
|
return G
|
||||||
@@ -434,6 +447,52 @@ def mvp(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def playertype(request: Annotated[TeamScopedRequest, Security(verify_team_scope)]):
|
||||||
|
with Session(engine) as session:
|
||||||
|
players = session.exec(
|
||||||
|
select(P)
|
||||||
|
.join(PlayerTeamLink)
|
||||||
|
.join(Team)
|
||||||
|
.where(Team.id == request.team_id, P.disabled == False)
|
||||||
|
).all()
|
||||||
|
if not players:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
player_map = {p.id: p for p in players}
|
||||||
|
subquery = (
|
||||||
|
select(PT.user, func.max(PT.time).label("latest"))
|
||||||
|
.where(PT.team == request.team_id)
|
||||||
|
.group_by(PT.user)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
statement2 = select(PT).join(
|
||||||
|
subquery, (PT.user == subquery.c.user) & (PT.time == subquery.c.latest)
|
||||||
|
)
|
||||||
|
playertypes = {}
|
||||||
|
for pt in session.exec(statement2):
|
||||||
|
for i, p_id in enumerate(pt.handlers):
|
||||||
|
if p_id not in player_map:
|
||||||
|
continue
|
||||||
|
playertypes[p_id] = playertypes.get(p_id, []) + [-1]
|
||||||
|
for i, p_id in enumerate(pt.combis):
|
||||||
|
if p_id not in player_map:
|
||||||
|
continue
|
||||||
|
playertypes[p_id] = playertypes.get(p_id, []) + [0]
|
||||||
|
for i, p_id in enumerate(pt.cutters):
|
||||||
|
if p_id not in player_map:
|
||||||
|
continue
|
||||||
|
playertypes[p_id] = playertypes.get(p_id, []) + [1]
|
||||||
|
playertype_analysis = {}
|
||||||
|
for p_id, v in playertypes.items():
|
||||||
|
v = np.array(v)
|
||||||
|
playertype_analysis[p_id] = {
|
||||||
|
"mean": np.mean(v),
|
||||||
|
"handler": (v == -1).sum() / len(v),
|
||||||
|
"combi": (v == 0).sum() / len(v),
|
||||||
|
"cutter": (v == 1).sum() / len(v),
|
||||||
|
}
|
||||||
|
return playertype_analysis
|
||||||
|
|
||||||
|
|
||||||
async def turnout(
|
async def turnout(
|
||||||
request: Annotated[
|
request: Annotated[
|
||||||
TeamScopedRequest, Security(verify_team_scope, scopes=["analysis"])
|
TeamScopedRequest, Security(verify_team_scope, scopes=["analysis"])
|
||||||
@@ -504,6 +563,9 @@ analysis_router.add_api_route(
|
|||||||
name="MVPs",
|
name="MVPs",
|
||||||
description="Request Most Valuable Players stats",
|
description="Request Most Valuable Players stats",
|
||||||
)
|
)
|
||||||
|
analysis_router.add_api_route(
|
||||||
|
"/playertype/{team_id}", endpoint=playertype, methods=["GET"]
|
||||||
|
)
|
||||||
analysis_router.add_api_route("/turnout/{team_id}", endpoint=turnout, methods=["GET"])
|
analysis_router.add_api_route("/turnout/{team_id}", endpoint=turnout, methods=["GET"])
|
||||||
analysis_router.add_api_route(
|
analysis_router.add_api_route(
|
||||||
"/times/{team_id}", endpoint=last_submissions, methods=["GET"]
|
"/times/{team_id}", endpoint=last_submissions, methods=["GET"]
|
||||||
|
|||||||
@@ -1,34 +1,18 @@
|
|||||||
import { JSX, useEffect, useState } from "react";
|
import { JSX, useEffect, useState } from "react";
|
||||||
import { apiAuth } from "./api";
|
import { apiAuth } from "./api";
|
||||||
import { useSession } from "./Session";
|
import { useSession } from "./Session";
|
||||||
|
import { Events } from "./types";
|
||||||
|
|
||||||
interface Datum {
|
const Calendar = ({
|
||||||
[id: number]: string;
|
playerId,
|
||||||
}
|
events,
|
||||||
interface Events {
|
}: {
|
||||||
[key: string]: Datum;
|
playerId: number;
|
||||||
}
|
events: Events | undefined;
|
||||||
|
}) => {
|
||||||
const Calendar = ({ playerId }: { playerId: number }) => {
|
|
||||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
const [events, setEvents] = useState<Events>();
|
|
||||||
const { teams, players } = useSession();
|
const { teams, players } = useSession();
|
||||||
|
|
||||||
async function loadSubmissionDates() {
|
|
||||||
if (teams?.activeTeam) {
|
|
||||||
const data = await apiAuth(`analysis/times/${teams?.activeTeam}`, null);
|
|
||||||
if (data.detail) {
|
|
||||||
console.log(data.detail);
|
|
||||||
} else {
|
|
||||||
setEvents(data as Events);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadSubmissionDates();
|
|
||||||
}, [players]);
|
|
||||||
|
|
||||||
const getEventsForDay = (date: Date) => {
|
const getEventsForDay = (date: Date) => {
|
||||||
return events && events[date.toISOString().split("T")[0]];
|
return events && events[date.toISOString().split("T")[0]];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const Join = () => {
|
|||||||
);
|
);
|
||||||
if (r.detail) setError(r.detail);
|
if (r.detail) setError(r.detail);
|
||||||
else {
|
else {
|
||||||
setTeams({ ...teams, activeTeam: teamID });
|
teamID && teams && setTeams({ ...teams, activeTeam: teamID });
|
||||||
navigate("/", { replace: true });
|
navigate("/", { replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export const GraphComponent = () => {
|
|||||||
const [showLikes, setShowLikes] = useState(true);
|
const [showLikes, setShowLikes] = useState(true);
|
||||||
const [showDislikes, setShowDislikes] = useState(false);
|
const [showDislikes, setShowDislikes] = useState(false);
|
||||||
const [popularity, setPopularity] = useState(false);
|
const [popularity, setPopularity] = useState(false);
|
||||||
|
const [showPlayerType, setShowPlayerType] = useState(false);
|
||||||
const [mutuality, setMutuality] = useState(false);
|
const [mutuality, setMutuality] = useState(false);
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
const { user, teams } = useSession();
|
const { user, teams } = useSession();
|
||||||
@@ -95,6 +96,10 @@ export const GraphComponent = () => {
|
|||||||
popularityLabel(!popularity);
|
popularityLabel(!popularity);
|
||||||
setPopularity(!popularity);
|
setPopularity(!popularity);
|
||||||
}
|
}
|
||||||
|
function handlePlayerType() {
|
||||||
|
playerType(!showPlayerType);
|
||||||
|
setShowPlayerType(!showPlayerType);
|
||||||
|
}
|
||||||
|
|
||||||
function handleMutuality() {
|
function handleMutuality() {
|
||||||
colorMatches(!mutuality);
|
colorMatches(!mutuality);
|
||||||
@@ -138,13 +143,25 @@ export const GraphComponent = () => {
|
|||||||
setData({ nodes: data.nodes, edges: newEdges });
|
setData({ nodes: data.nodes, edges: newEdges });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playerType(popularity: boolean) {
|
||||||
|
const newNodes = data.nodes;
|
||||||
|
if (popularity) {
|
||||||
|
newNodes.forEach((node) => {
|
||||||
|
node.fill = node.data.playertype;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newNodes.forEach((node) => (node.fill = undefined));
|
||||||
|
}
|
||||||
|
setData({ nodes: newNodes, edges: data.edges });
|
||||||
|
}
|
||||||
|
|
||||||
function popularityLabel(popularity: boolean) {
|
function popularityLabel(popularity: boolean) {
|
||||||
const newNodes = data.nodes;
|
const newNodes = data.nodes;
|
||||||
console.log(data.nodes);
|
|
||||||
if (popularity) {
|
if (popularity) {
|
||||||
newNodes.forEach(
|
newNodes.forEach((node) => {
|
||||||
(node) => (node.subLabel = `pop.: ${node.data.inDegree.toFixed(1)}`)
|
node.subLabel = `pop.: ${node.data.inDegree.toFixed(1)}`;
|
||||||
);
|
node.fill = node.data.playertype;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
newNodes.forEach((node) => (node.subLabel = undefined));
|
newNodes.forEach((node) => (node.subLabel = undefined));
|
||||||
}
|
}
|
||||||
@@ -305,6 +322,51 @@ export const GraphComponent = () => {
|
|||||||
<span className="ml-1">popularity</span>
|
<span className="ml-1">popularity</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="control">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showPlayerType}
|
||||||
|
onClick={handlePlayerType}
|
||||||
|
/>
|
||||||
|
<span className="ml-1">player type</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="control pl-4">
|
||||||
|
{showPlayerType && (
|
||||||
|
<>
|
||||||
|
<p>RGB:</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
color: "red",
|
||||||
|
fontWeight: "bold",
|
||||||
|
paddingLeft: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
handler
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
color: "green",
|
||||||
|
fontWeight: "bold",
|
||||||
|
paddingLeft: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
combi
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
color: "blue",
|
||||||
|
fontWeight: "bold",
|
||||||
|
paddingLeft: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
cutter
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FormEvent, useEffect, useState } from "react";
|
import { FormEvent, useEffect, useState } from "react";
|
||||||
import { apiAuth, Gender, User } from "./api";
|
import { apiAuth, Gender, User } from "./api";
|
||||||
import { useSession } from "./Session";
|
import { useSession } from "./Session";
|
||||||
import { ErrorState } from "./types";
|
import { ErrorState, Events } from "./types";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import Calendar from "./Calendar";
|
import Calendar from "./Calendar";
|
||||||
import { Info, Star, StarHalf, StarOff, UserPen } from "lucide-react";
|
import { Info, Star, StarHalf, StarOff, UserPen } from "lucide-react";
|
||||||
@@ -9,12 +9,30 @@ import Loading from "./Loading";
|
|||||||
|
|
||||||
const TeamPanel = () => {
|
const TeamPanel = () => {
|
||||||
const { user, teams, players, reloadPlayers } = useSession();
|
const { user, teams, players, reloadPlayers } = useSession();
|
||||||
|
const [events, setEvents] = useState<Events>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
user?.scopes.includes(`team:${teams?.activeTeam}`) ||
|
user?.scopes.includes(`team:${teams?.activeTeam}`) ||
|
||||||
teams?.activeTeam === 42 ||
|
teams?.activeTeam === 42 ||
|
||||||
navigate("/", { replace: true });
|
navigate("/", { replace: true });
|
||||||
}, [user, teams]);
|
}, [user, teams]);
|
||||||
|
|
||||||
|
async function loadSubmissionDates() {
|
||||||
|
if (teams?.activeTeam) {
|
||||||
|
const data = await apiAuth(`analysis/times/${teams?.activeTeam}`, null);
|
||||||
|
if (data.detail) {
|
||||||
|
console.log(data.detail);
|
||||||
|
} else {
|
||||||
|
setEvents(data as Events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadSubmissionDates();
|
||||||
|
}, [players]);
|
||||||
|
|
||||||
const newPlayerTemplate = {
|
const newPlayerTemplate = {
|
||||||
id: 0,
|
id: 0,
|
||||||
username: "",
|
username: "",
|
||||||
@@ -27,6 +45,16 @@ const TeamPanel = () => {
|
|||||||
const [error, setError] = useState<ErrorState>();
|
const [error, setError] = useState<ErrorState>();
|
||||||
const [player, setPlayer] = useState(newPlayerTemplate);
|
const [player, setPlayer] = useState(newPlayerTemplate);
|
||||||
|
|
||||||
|
const getPlayerSubmissions = () => {
|
||||||
|
var submissions = [];
|
||||||
|
if (events) {
|
||||||
|
for (const [date, obj] of Object.entries(events))
|
||||||
|
for (const [id, emoji] of Object.entries(obj))
|
||||||
|
if (player.id === Number(id)) submissions.push({ emoji, date });
|
||||||
|
}
|
||||||
|
return submissions;
|
||||||
|
};
|
||||||
|
|
||||||
async function handleSubmit(e: FormEvent) {
|
async function handleSubmit(e: FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (teams) {
|
if (teams) {
|
||||||
@@ -252,6 +280,21 @@ const TeamPanel = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{player.id !== 0 && (
|
||||||
|
<div className="field">
|
||||||
|
<div className="control">
|
||||||
|
<label className="label">submissions</label>
|
||||||
|
{getPlayerSubmissions().map((submission) => (
|
||||||
|
<span className="tooltip">
|
||||||
|
{submission.emoji}
|
||||||
|
<span className="tooltiptext notification is-primary is-light has-text-centered">
|
||||||
|
{submission.date}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{error?.message && (
|
{error?.message && (
|
||||||
<p
|
<p
|
||||||
@@ -287,7 +330,7 @@ const TeamPanel = () => {
|
|||||||
</section>
|
</section>
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<Calendar playerId={player.id} />
|
<Calendar playerId={player.id} events={events} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -30,7 +30,9 @@
|
|||||||
/* Tooltip text */
|
/* Tooltip text */
|
||||||
.tooltiptext {
|
.tooltiptext {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
width: 10rem;
|
top: 1.5rem;
|
||||||
|
left: -0.5rem;
|
||||||
|
width: 8rem;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|||||||
@@ -53,3 +53,10 @@ export type ErrorState = {
|
|||||||
ok: boolean;
|
ok: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface Datum {
|
||||||
|
[id: number]: string;
|
||||||
|
}
|
||||||
|
export interface Events {
|
||||||
|
[key: string]: Datum;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user