Fix the frontend API calls and implement logins on frontend #7
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import ForceGraph3D from "react-force-graph-3d";
|
import ForceGraph3D from "react-force-graph-3d";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -6,12 +7,19 @@ import {
|
|||||||
} from '../types/ApiTypes';
|
} from '../types/ApiTypes';
|
||||||
|
|
||||||
import StatsStyling from "../styles/stats_styling";
|
import StatsStyling from "../styles/stats_styling";
|
||||||
|
import Card from "./Card";
|
||||||
|
|
||||||
const styles = StatsStyling;
|
const styles = StatsStyling;
|
||||||
|
|
||||||
|
type GraphLink = {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
|
||||||
function ApiToGraphData(apiData: InteractionGraph) {
|
function ApiToGraphData(apiData: InteractionGraph) {
|
||||||
const nodes = Object.keys(apiData).map(username => ({ id: username }));
|
const nodes = Object.keys(apiData).map(username => ({ id: username }));
|
||||||
const links = [];
|
const links: GraphLink[] = [];
|
||||||
|
|
||||||
for (const [source, targets] of Object.entries(apiData)) {
|
for (const [source, targets] of Object.entries(apiData)) {
|
||||||
for (const [target, count] of Object.entries(targets)) {
|
for (const [target, count] of Object.entries(targets)) {
|
||||||
@@ -35,26 +43,105 @@ function ApiToGraphData(apiData: InteractionGraph) {
|
|||||||
|
|
||||||
|
|
||||||
const UserStats = (props: { data: UserAnalysisResponse }) => {
|
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 (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
|
<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>
|
<h2 style={styles.sectionTitle}>User Interaction Graph</h2>
|
||||||
<p style={styles.sectionSubtitle}>
|
<p style={styles.sectionSubtitle}>
|
||||||
This graph visualizes interactions between users based on comments and replies.
|
Nodes represent users and links represent conversation interactions.
|
||||||
Nodes represent users, and edges represent interactions (e.g., comments or replies) between them.
|
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div ref={graphContainerRef} style={{ width: "100%", height: graphSize.height }}>
|
||||||
<ForceGraph3D
|
<ForceGraph3D
|
||||||
|
width={graphSize.width}
|
||||||
|
height={graphSize.height}
|
||||||
graphData={graphData}
|
graphData={graphData}
|
||||||
nodeAutoColorBy="id"
|
nodeAutoColorBy="id"
|
||||||
linkDirectionalParticles={2}
|
linkDirectionalParticles={1}
|
||||||
linkDirectionalParticleSpeed={0.005}
|
linkDirectionalParticleSpeed={0.004}
|
||||||
linkWidth={(link) => Math.sqrt(link.value)}
|
linkWidth={(link) => Math.sqrt(Number(link.value))}
|
||||||
nodeLabel={(node) => `${node.id}`}
|
nodeLabel={(node) => `${node.id}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useParams } from "react-router-dom";
|
|||||||
import StatsStyling from "../styles/stats_styling";
|
import StatsStyling from "../styles/stats_styling";
|
||||||
import SummaryStats from "../components/SummaryStats";
|
import SummaryStats from "../components/SummaryStats";
|
||||||
import EmotionalStats from "../components/EmotionalStats";
|
import EmotionalStats from "../components/EmotionalStats";
|
||||||
import InteractionStats from "../components/UserStats";
|
import UserStats from "../components/UserStats";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type SummaryResponse,
|
type SummaryResponse,
|
||||||
@@ -20,7 +20,7 @@ const StatPage = () => {
|
|||||||
const { datasetId: routeDatasetId } = useParams<{ datasetId: string }>();
|
const { datasetId: routeDatasetId } = useParams<{ datasetId: string }>();
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
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<UserAnalysisResponse | null>(null);
|
const [userData, setUserData] = useState<UserAnalysisResponse | null>(null);
|
||||||
const [timeData, setTimeData] = useState<TimeAnalysisResponse | null>(null);
|
const [timeData, setTimeData] = useState<TimeAnalysisResponse | null>(null);
|
||||||
@@ -213,10 +213,10 @@ return (
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveView("interaction")}
|
onClick={() => setActiveView("user")}
|
||||||
style={activeView === "interaction" ? styles.buttonPrimary : styles.buttonSecondary}
|
style={activeView === "user" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||||
>
|
>
|
||||||
Interaction
|
Users
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -239,8 +239,8 @@ return (
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeView === "interaction" && userData && (
|
{activeView === "user" && userData && (
|
||||||
<InteractionStats data={userData} />
|
<UserStats data={userData} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user