feat(user stats): updated styling and stats in user page

Interaction graph was taking up too much space and was the only thing on the screen. Further statistics were added however these may be removed in favour of more informative statistics
This commit is contained in:
2026-03-04 20:20:34 +00:00
parent 24277e0104
commit 5fc1f1532f
2 changed files with 108 additions and 21 deletions

View File

@@ -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<HTMLDivElement | null>(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<GraphLink | null>((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 (
<div style={styles.page}>
<h2 style={styles.sectionTitle}>User Interaction Graph</h2>
<p style={styles.sectionSubtitle}>
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.
</p>
<div>
<ForceGraph3D
<div style={{ ...styles.container, ...styles.grid }}>
<Card
label="Users"
value={totalUsers.toLocaleString()}
sublabel={`${connectedUsers.toLocaleString()} users in filtered graph`}
style={{ gridColumn: "span 3" }}
/>
<Card
label="Interactions"
value={totalInteractions.toLocaleString()}
sublabel="Filtered links (2+ interactions)"
style={{ gridColumn: "span 3" }}
/>
<Card
label="Average Intensity"
value={avgInteractionsPerConnectedUser.toFixed(1)}
sublabel="Interactions per connected user"
style={{ gridColumn: "span 3" }}
/>
<Card
label="Most Active User"
value={mostActiveUser?.author ?? "—"}
sublabel={mostActiveUser ? `${mostActiveUser.count.toLocaleString()} events` : "No user activity found"}
style={{ gridColumn: "span 3" }}
/>
<Card
label="Strongest Connection"
value={strongestLink ? `${strongestLink.source} -> ${strongestLink.target}` : "—"}
sublabel={strongestLink ? `${strongestLink.value.toLocaleString()} interactions` : "No graph edges after filtering"}
style={{ gridColumn: "span 6" }}
/>
<Card
label="Most Reply-Driven User"
value={highlyInteractiveUser?.author ?? "—"}
sublabel={
highlyInteractiveUser
? `${Math.round(highlyInteractiveUser.comment_share * 100)}% comments`
: "No user distribution available"
}
style={{ gridColumn: "span 6" }}
/>
<div style={{ ...styles.card, gridColumn: "span 12" }}>
<h2 style={styles.sectionTitle}>User Interaction Graph</h2>
<p style={styles.sectionSubtitle}>
Nodes represent users and links represent conversation interactions.
</p>
<div ref={graphContainerRef} style={{ width: "100%", height: graphSize.height }}>
<ForceGraph3D
width={graphSize.width}
height={graphSize.height}
graphData={graphData}
nodeAutoColorBy="id"
linkDirectionalParticles={2}
linkDirectionalParticleSpeed={0.005}
linkWidth={(link) => Math.sqrt(link.value)}
linkDirectionalParticles={1}
linkDirectionalParticleSpeed={0.004}
linkWidth={(link) => Math.sqrt(Number(link.value))}
nodeLabel={(node) => `${node.id}`}
/>
/>
</div>
</div>
</div>
</div>
);
}
export default UserStats;
export default UserStats;