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 a290c7f..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); @@ -213,10 +213,10 @@ return ( @@ -239,8 +239,8 @@ return ( )} - {activeView === "interaction" && userData && ( - + {activeView === "user" && userData && ( + )}