feat(frontend): add 3d interaction graph

This commit is contained in:
2026-02-16 17:03:51 +00:00
parent 7c1e069152
commit 4f577abd4f
6 changed files with 385 additions and 3 deletions

View File

@@ -0,0 +1,50 @@
import ForceGraph3D from "react-force-graph-3d";
import {
type UserAnalysisResponse,
type InteractionGraph
} from '../types/ApiTypes';
import StatsStyling from "../styles/stats_styling";
const styles = StatsStyling;
function ApiToGraphData(apiData: InteractionGraph) {
const nodes = Object.keys(apiData).map(username => ({ id: username }));
const links = [];
for (const [source, targets] of Object.entries(apiData)) {
for (const [target, count] of Object.entries(targets)) {
links.push({ source, target, value: count });
}
}
return { nodes, links };
}
const InteractionStats = (props: { data: UserAnalysisResponse }) => {
const graphData = ApiToGraphData(props.data.interaction_graph);
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 style={{ height: "600px", border: "1px solid #ccc", borderRadius: 8, marginTop: 16 }}>
<ForceGraph3D
graphData={graphData}
nodeAutoColorBy="id"
linkDirectionalParticles={2}
linkDirectionalParticleSpeed={0.005}
linkWidth={(link) => Math.sqrt(link.value)}
nodeLabel={(node) => `${node.id}`}
/>
</div>
</div>
);
}
export default InteractionStats;

View File

@@ -3,6 +3,7 @@ import axios from "axios";
import StatsStyling from "../styles/stats_styling";
import SummaryStats from "../components/SummaryStats";
import EmotionalStats from "../components/EmotionalStats";
import InteractionStats from "../components/InteractionStats";
import {
type SummaryResponse,
@@ -16,7 +17,7 @@ const styles = StatsStyling;
const StatPage = () => {
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [activeView, setActiveView] = useState<"summary" | "emotional">("summary");
const [activeView, setActiveView] = useState<"summary" | "emotional" | "interaction">("summary");
const [userData, setUserData] = useState<UserAnalysisResponse | null>(null);
const [timeData, setTimeData] = useState<TimeAnalysisResponse | null>(null);
@@ -133,6 +134,13 @@ return (
>
Emotional
</button>
<button
onClick={() => setActiveView("interaction")}
style={activeView === "interaction" ? styles.buttonPrimary : styles.buttonSecondary}
>
Interaction
</button>
</div>
{activeView === "summary" && (
@@ -154,6 +162,10 @@ return (
</div>
)}
{activeView === "interaction" && userData && (
<InteractionStats data={userData} />
)}
</div>
);
}

View File

@@ -35,9 +35,12 @@ type User = {
vocab?: Vocab | null;
};
type InteractionGraph = Record<string, Record<string, number>>;
type UserAnalysisResponse = {
top_users: TopUser[];
users: User[];
interaction_graph: InteractionGraph;
};
// Time Analysis
@@ -89,6 +92,7 @@ export type {
TopUser,
Vocab,
User,
InteractionGraph,
UserAnalysisResponse,
FrequencyWord,
AverageEmotionByTopic,