feat(frontend): add 3d interaction graph
This commit is contained in:
50
frontend/src/components/InteractionStats.tsx
Normal file
50
frontend/src/components/InteractionStats.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user