feat: simple analysis page with sociogram
This commit is contained in:
parent
8def52fbf2
commit
25bda2bc4d
144
analysis.py
Normal file
144
analysis.py
Normal file
@ -0,0 +1,144 @@
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
import io
|
||||
import base64
|
||||
from fastapi import APIRouter
|
||||
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
|
||||
import networkx as nx
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
analysis_router = APIRouter(prefix="/analysis")
|
||||
|
||||
|
||||
C = Chemistry
|
||||
P = Player
|
||||
|
||||
|
||||
def sociogram_json():
|
||||
nodes = []
|
||||
necessary_nodes = set()
|
||||
links = []
|
||||
with Session(engine) as session:
|
||||
for p in session.exec(select(P)).fetchall():
|
||||
nodes.append({"id": p.name, "appearance": 1})
|
||||
subquery = (
|
||||
select(C.user, func.max(C.time).label("latest"))
|
||||
.where(C.time > datetime(2025, 2, 1, 10))
|
||||
.group_by(C.user)
|
||||
.subquery()
|
||||
)
|
||||
statement2 = select(C).join(
|
||||
subquery, (C.user == subquery.c.user) & (C.time == subquery.c.latest)
|
||||
)
|
||||
for c in session.exec(statement2):
|
||||
# G.add_node(c.user)
|
||||
necessary_nodes.add(c.user)
|
||||
for p in c.love:
|
||||
# G.add_edge(c.user, p)
|
||||
# p_id = session.exec(select(P.id).where(P.name == p)).one()
|
||||
necessary_nodes.add(p)
|
||||
links.append({"source": c.user, "target": p})
|
||||
# nodes = [n for n in nodes if n["name"] in necessary_nodes]
|
||||
return JSONResponse({"nodes": nodes, "links": links})
|
||||
|
||||
|
||||
def sociogram_data():
|
||||
nodes = []
|
||||
links = []
|
||||
G = nx.DiGraph()
|
||||
with Session(engine) as session:
|
||||
for p in session.exec(select(P)).fetchall():
|
||||
nodes.append({"id": p.name})
|
||||
G.add_node(p.name)
|
||||
subquery = (
|
||||
select(C.user, func.max(C.time).label("latest"))
|
||||
.where(C.time > datetime(2025, 2, 1, 10))
|
||||
.group_by(C.user)
|
||||
.subquery()
|
||||
)
|
||||
statement2 = (
|
||||
select(C)
|
||||
.where(C.user.in_(["Kruse", "Franz", "ck"]))
|
||||
.join(subquery, (C.user == subquery.c.user) & (C.time == subquery.c.latest))
|
||||
)
|
||||
for c in session.exec(statement2):
|
||||
for p in c.love:
|
||||
G.add_edge(c.user, p)
|
||||
links.append({"source": c.user, "target": p})
|
||||
return G
|
||||
|
||||
|
||||
class Params(BaseModel):
|
||||
node_size: int | None = Field(default=2400, alias="nodeSize")
|
||||
font_size: int | None = Field(default=10, alias="fontSize")
|
||||
arrow_size: int | None = Field(default=20, alias="arrowSize")
|
||||
edge_width: float | None = Field(default=1, alias="edgeWidth")
|
||||
|
||||
|
||||
def sociogram_image(params: Params):
|
||||
print(params)
|
||||
plt.figure(figsize=(16, 10), facecolor="none")
|
||||
ax = plt.gca()
|
||||
ax.set_facecolor("none") # Set the axis face color to none (transparent)
|
||||
ax.axis("off") # Turn off axis ticks and frames
|
||||
|
||||
G = sociogram_data()
|
||||
pos = nx.spring_layout(
|
||||
G, scale=2, k=1 / np.sqrt(G.number_of_edges()), iterations=50, seed=42
|
||||
)
|
||||
nx.draw_networkx_nodes(
|
||||
G,
|
||||
pos,
|
||||
node_color="#99ccff",
|
||||
edgecolors="#404040",
|
||||
linewidths=1,
|
||||
node_size=params.node_size,
|
||||
alpha=0.86,
|
||||
)
|
||||
nx.draw_networkx_labels(G, pos, font_size=params.font_size)
|
||||
nx.draw_networkx_edges(
|
||||
G,
|
||||
pos,
|
||||
arrows=True,
|
||||
edge_color="#404040",
|
||||
arrowsize=params.arrow_size,
|
||||
node_size=params.node_size,
|
||||
width=params.edge_width,
|
||||
)
|
||||
|
||||
buf = io.BytesIO()
|
||||
plt.savefig(buf, format="png", bbox_inches="tight", dpi=300, transparent=True)
|
||||
buf.seek(0)
|
||||
encoded_image = base64.b64encode(buf.read()).decode("UTF-8")
|
||||
plt.close()
|
||||
|
||||
return {"image": encoded_image}
|
||||
|
||||
|
||||
analysis_router.add_api_route("/json", endpoint=sociogram_json, methods=["GET"])
|
||||
analysis_router.add_api_route("/image", endpoint=sociogram_image, methods=["POST"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
with Session(engine) as session:
|
||||
statement: SelectOfScalar[P] = select(func.count(P.id))
|
||||
print("players in DB: ", session.exec(statement).first())
|
||||
G = sociogram_data()
|
||||
pos = nx.spring_layout(G, scale=1, k=2, iterations=50, seed=42)
|
||||
edges = nx.draw_networkx_edges(
|
||||
G,
|
||||
pos,
|
||||
arrows=True,
|
||||
arrowsize=12,
|
||||
)
|
||||
nx.draw_networkx(
|
||||
G, pos, with_labels=True, node_color="#99ccff", font_size=8, node_size=2000
|
||||
)
|
||||
plt.show()
|
2
main.py
2
main.py
@ -6,6 +6,7 @@ from sqlmodel import (
|
||||
select,
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from analysis import analysis_router
|
||||
|
||||
|
||||
app = FastAPI(title="cutt")
|
||||
@ -81,4 +82,5 @@ def submit_chemistry(chemistry: Chemistry):
|
||||
|
||||
app.include_router(player_router)
|
||||
app.include_router(team_router)
|
||||
app.include_router(analysis_router)
|
||||
app.mount("/", StaticFiles(directory="dist", html=True), name="site")
|
||||
|
@ -27,8 +27,14 @@
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"react-router-dom": "^7.1.5",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
"vite": "^6.0.5"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true
|
||||
}
|
||||
}
|
||||
|
54
src/App.css
54
src/App.css
@ -45,6 +45,30 @@ h3 {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: flex;
|
||||
|
||||
button,
|
||||
img {
|
||||
padding: 0 1em;
|
||||
margin: 3px auto;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
#control-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
input {
|
||||
margin: auto
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
@ -61,10 +85,12 @@ h3 {
|
||||
.box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
&.one {
|
||||
max-width: min(96%, 768px);
|
||||
margin: 4px auto;
|
||||
}
|
||||
|
||||
padding: 4px;
|
||||
margin: 4px 0.5%;
|
||||
border-style: solid;
|
||||
@ -82,6 +108,7 @@ h3 {
|
||||
max-width: 240px;
|
||||
min-width: 100px;
|
||||
margin: 4px auto;
|
||||
|
||||
.item {
|
||||
font-weight: bold;
|
||||
border-style: solid;
|
||||
@ -115,26 +142,26 @@ button {
|
||||
.submit_text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.submit {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
padding: 0px;
|
||||
background-color: unset;
|
||||
padding: 0.4em;
|
||||
border-radius: 1em;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
font-size: xx-large;
|
||||
margin-bottom: 20px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
magenta,
|
||||
rebeccapurple,
|
||||
dodgerblue,
|
||||
green
|
||||
);
|
||||
background-image: linear-gradient(45deg,
|
||||
magenta,
|
||||
rebeccapurple,
|
||||
dodgerblue,
|
||||
green);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
@ -152,6 +179,7 @@ button {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
opacity: 50%;
|
||||
|
||||
&:hover {
|
||||
opacity: 75%;
|
||||
}
|
||||
@ -178,10 +206,12 @@ button {
|
||||
text-align: center;
|
||||
height: 140px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
position: absolute;
|
||||
font-size: medium;
|
||||
@ -206,6 +236,7 @@ button {
|
||||
border: 4px solid black;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loader::after {
|
||||
content: "";
|
||||
width: 32%;
|
||||
@ -223,6 +254,7 @@ button {
|
||||
left: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
left: 100%;
|
||||
transform: translateX(0%);
|
||||
|
40
src/App.tsx
40
src/App.tsx
@ -1,31 +1,27 @@
|
||||
import Analysis from "./Analysis";
|
||||
import { baseUrl } from "./api";
|
||||
import "./App.css";
|
||||
import Footer from "./Footer";
|
||||
import Header from "./Header";
|
||||
import Rankings from "./Rankings";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
//const [data, setData] = useState({ nodes: [], links: [] } as SociogramData);
|
||||
//async function loadData() {
|
||||
// await fetch(`${baseUrl}analysis/json`, { method: "GET" }).then(resp => resp.json() as unknown as SociogramData).then(json => { setData(json) })
|
||||
//}
|
||||
//useEffect(() => { loadData() }, [])
|
||||
//
|
||||
return (
|
||||
<>
|
||||
<div className="logo">
|
||||
<a href={baseUrl}>
|
||||
<img alt="logo" height="66%" src="logo.svg" />
|
||||
<h3 className="centered">cutt</h3>
|
||||
</a>
|
||||
<span className="grey">cool ultimate team tool</span>
|
||||
</div>
|
||||
<Rankings />
|
||||
<footer>
|
||||
<p className="grey">
|
||||
something not working?
|
||||
<br />
|
||||
message <a href="https://t.me/x0124816">me</a>.
|
||||
<br />
|
||||
or fix it here:{" "}
|
||||
<a href="https://git.0124816.xyz/julius/cutt" key="gitea">
|
||||
<img src="gitea.svg" alt="gitea" height="16" />
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</>
|
||||
<BrowserRouter>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route index element={<Rankings />} />
|
||||
<Route path="analysis" element={<Analysis />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
export default App;
|
||||
|
@ -136,8 +136,7 @@ export function Chemistry({ user, players }: PlayerInfoProps) {
|
||||
<h2>😬</h2>
|
||||
{playersLeft.length < 1 && (
|
||||
<span className="grey hint">
|
||||
drag people here that you'd rather not play with from worst to ...
|
||||
ok
|
||||
drag people here that you'd rather not play with
|
||||
</span>
|
||||
)}
|
||||
<PlayerList
|
||||
@ -145,7 +144,6 @@ export function Chemistry({ user, players }: PlayerInfoProps) {
|
||||
setList={setPlayersLeft}
|
||||
group={"shared"}
|
||||
className="dragbox"
|
||||
orderedList
|
||||
/>
|
||||
</div>
|
||||
<div className="box three">
|
||||
|
Loading…
x
Reference in New Issue
Block a user