Editable and removable datasets #8
@@ -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,26 +43,105 @@ 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}>
|
||||
<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}>
|
||||
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.
|
||||
Nodes represent users and links represent conversation interactions.
|
||||
</p>
|
||||
<div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<UserAnalysisResponse | null>(null);
|
||||
const [timeData, setTimeData] = useState<TimeAnalysisResponse | null>(null);
|
||||
@@ -139,7 +139,7 @@ const StatPage = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={styles.loadingPage}>
|
||||
<div style={styles.loadingCard}>
|
||||
<div style={{ ...styles.loadingCard, transform: "translateY(-100px)" }}>
|
||||
<div style={styles.loadingHeader}>
|
||||
<div style={styles.loadingSpinner} />
|
||||
<div>
|
||||
@@ -213,10 +213,10 @@ return (
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveView("interaction")}
|
||||
style={activeView === "interaction" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||
onClick={() => setActiveView("user")}
|
||||
style={activeView === "user" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||
>
|
||||
Interaction
|
||||
Users
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -239,8 +239,8 @@ return (
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeView === "interaction" && userData && (
|
||||
<InteractionStats data={userData} />
|
||||
{activeView === "user" && userData && (
|
||||
<UserStats data={userData} />
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,17 +2,15 @@ import pandas as pd
|
||||
|
||||
from server.queue.celery_app import celery
|
||||
from server.analysis.enrichment import DatasetEnrichment
|
||||
from server.db.database import PostgresConnector
|
||||
from server.core.datasets import DatasetManager
|
||||
|
||||
@celery.task(bind=True, max_retries=3)
|
||||
def process_dataset(self, dataset_id: int, posts: list, topics: dict):
|
||||
|
||||
try:
|
||||
from server.db.database import PostgresConnector
|
||||
from server.core.datasets import DatasetManager
|
||||
|
||||
db = PostgresConnector()
|
||||
dataset_manager = DatasetManager(db)
|
||||
|
||||
try:
|
||||
df = pd.DataFrame(posts)
|
||||
|
||||
processor = DatasetEnrichment(df, topics)
|
||||
|
||||
Reference in New Issue
Block a user