diff --git a/frontend/src/components/UserStats.tsx b/frontend/src/components/UserStats.tsx index b33fcd4..bb060cc 100644 --- a/frontend/src/components/UserStats.tsx +++ b/frontend/src/components/UserStats.tsx @@ -1,3 +1,4 @@ +import { useEffect, useMemo, useRef, useState } from "react"; import ForceGraph3D from "react-force-graph-3d"; import { @@ -6,12 +7,19 @@ import { } from '../types/ApiTypes'; import StatsStyling from "../styles/stats_styling"; +import Card from "./Card"; const styles = StatsStyling; +type GraphLink = { + source: string; + target: string; + value: number; +}; + function ApiToGraphData(apiData: InteractionGraph) { const nodes = Object.keys(apiData).map(username => ({ id: username })); - const links = []; + const links: GraphLink[] = []; for (const [source, targets] of Object.entries(apiData)) { for (const [target, count] of Object.entries(targets)) { @@ -35,27 +43,106 @@ function ApiToGraphData(apiData: InteractionGraph) { const UserStats = (props: { data: UserAnalysisResponse }) => { - const graphData = ApiToGraphData(props.data.interaction_graph); + const graphData = useMemo(() => ApiToGraphData(props.data.interaction_graph), [props.data.interaction_graph]); + const graphContainerRef = useRef(null); + const [graphSize, setGraphSize] = useState({ width: 720, height: 540 }); + + useEffect(() => { + const updateGraphSize = () => { + const containerWidth = graphContainerRef.current?.clientWidth ?? 720; + const nextWidth = Math.max(320, Math.floor(containerWidth)); + const nextHeight = nextWidth < 700 ? 300 : 540; + setGraphSize({ width: nextWidth, height: nextHeight }); + }; + + updateGraphSize(); + window.addEventListener("resize", updateGraphSize); + + return () => window.removeEventListener("resize", updateGraphSize); + }, []); + + const totalUsers = props.data.users.length; + const connectedUsers = graphData.nodes.length; + const totalInteractions = graphData.links.reduce((sum, link) => sum + link.value, 0); + const avgInteractionsPerConnectedUser = connectedUsers ? totalInteractions / connectedUsers : 0; + + const strongestLink = graphData.links.reduce((best, current) => { + if (!best || current.value > best.value) { + return current; + } + return best; + }, null); + + const highlyInteractiveUser = [...props.data.users].sort((a, b) => b.comment_share - a.comment_share)[0]; + + const mostActiveUser = props.data.top_users.find(u => u.author !== "[deleted]"); return (
-

User Interaction Graph

-

- This graph visualizes interactions between users based on comments and replies. - Nodes represent users, and edges represent interactions (e.g., comments or replies) between them. -

-
- + + + + + + ${strongestLink.target}` : "—"} + sublabel={strongestLink ? `${strongestLink.value.toLocaleString()} interactions` : "No graph edges after filtering"} + style={{ gridColumn: "span 6" }} + /> + + +
+

User Interaction Graph

+

+ Nodes represent users and links represent conversation interactions. +

+
+ Math.sqrt(link.value)} + linkDirectionalParticles={1} + linkDirectionalParticleSpeed={0.004} + linkWidth={(link) => Math.sqrt(Number(link.value))} nodeLabel={(node) => `${node.id}`} - /> + /> +
+
); } -export default UserStats; \ No newline at end of file +export default UserStats; diff --git a/frontend/src/pages/Stats.tsx b/frontend/src/pages/Stats.tsx index 586d6c2..683584f 100644 --- a/frontend/src/pages/Stats.tsx +++ b/frontend/src/pages/Stats.tsx @@ -4,7 +4,7 @@ import { useParams } from "react-router-dom"; import StatsStyling from "../styles/stats_styling"; import SummaryStats from "../components/SummaryStats"; import EmotionalStats from "../components/EmotionalStats"; -import InteractionStats from "../components/UserStats"; +import UserStats from "../components/UserStats"; import { type SummaryResponse, @@ -20,7 +20,7 @@ const StatPage = () => { const { datasetId: routeDatasetId } = useParams<{ datasetId: string }>(); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); - const [activeView, setActiveView] = useState<"summary" | "emotional" | "interaction">("summary"); + const [activeView, setActiveView] = useState<"summary" | "emotional" | "user">("summary"); const [userData, setUserData] = useState(null); const [timeData, setTimeData] = useState(null); @@ -139,7 +139,7 @@ const StatPage = () => { if (loading) { return (
-
+
@@ -213,10 +213,10 @@ return (
@@ -239,8 +239,8 @@ return (
)} - {activeView === "interaction" && userData && ( - + {activeView === "user" && userData && ( + )}
diff --git a/server/queue/tasks.py b/server/queue/tasks.py index f2f3268..a089596 100644 --- a/server/queue/tasks.py +++ b/server/queue/tasks.py @@ -2,17 +2,15 @@ import pandas as pd from server.queue.celery_app import celery from server.analysis.enrichment import DatasetEnrichment +from server.db.database import PostgresConnector +from server.core.datasets import DatasetManager @celery.task(bind=True, max_retries=3) def process_dataset(self, dataset_id: int, posts: list, topics: dict): + db = PostgresConnector() + dataset_manager = DatasetManager(db) try: - from server.db.database import PostgresConnector - from server.core.datasets import DatasetManager - - db = PostgresConnector() - dataset_manager = DatasetManager(db) - df = pd.DataFrame(posts) processor = DatasetEnrichment(df, topics)