style: run prettifier plugin on entire frontend
This commit is contained in:
@@ -2,9 +2,9 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import ForceGraph3D from "react-force-graph-3d";
|
||||
|
||||
import {
|
||||
type UserAnalysisResponse,
|
||||
type InteractionGraph
|
||||
} from '../types/ApiTypes';
|
||||
type UserAnalysisResponse,
|
||||
type InteractionGraph,
|
||||
} from "../types/ApiTypes";
|
||||
|
||||
import StatsStyling from "../styles/stats_styling";
|
||||
import Card from "./Card";
|
||||
@@ -12,38 +12,43 @@ import Card from "./Card";
|
||||
const styles = StatsStyling;
|
||||
|
||||
type GraphLink = {
|
||||
source: string;
|
||||
target: string;
|
||||
value: number;
|
||||
source: string;
|
||||
target: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
function ApiToGraphData(apiData: InteractionGraph) {
|
||||
const nodes = Object.keys(apiData).map(username => ({ id: username }));
|
||||
const links: GraphLink[] = [];
|
||||
|
||||
for (const [source, targets] of Object.entries(apiData)) {
|
||||
for (const [target, count] of Object.entries(targets)) {
|
||||
links.push({ source, target, value: count });
|
||||
}
|
||||
const nodes = Object.keys(apiData).map((username) => ({ id: username }));
|
||||
const links: GraphLink[] = [];
|
||||
|
||||
for (const [source, targets] of Object.entries(apiData)) {
|
||||
for (const [target, count] of Object.entries(targets)) {
|
||||
links.push({ source, target, value: count });
|
||||
}
|
||||
|
||||
// drop low-value and deleted interactions to reduce clutter
|
||||
const filteredLinks = links.filter(link =>
|
||||
link.value >= 2 &&
|
||||
link.source !== "[deleted]" &&
|
||||
link.target !== "[deleted]"
|
||||
);
|
||||
}
|
||||
|
||||
// also filter out nodes that are no longer connected after link filtering
|
||||
const connectedNodeIds = new Set(filteredLinks.flatMap(link => [link.source, link.target]));
|
||||
const filteredNodes = nodes.filter(node => connectedNodeIds.has(node.id));
|
||||
// drop low-value and deleted interactions to reduce clutter
|
||||
const filteredLinks = links.filter(
|
||||
(link) =>
|
||||
link.value >= 2 &&
|
||||
link.source !== "[deleted]" &&
|
||||
link.target !== "[deleted]",
|
||||
);
|
||||
|
||||
return { nodes: filteredNodes, links: filteredLinks};
|
||||
// also filter out nodes that are no longer connected after link filtering
|
||||
const connectedNodeIds = new Set(
|
||||
filteredLinks.flatMap((link) => [link.source, link.target]),
|
||||
);
|
||||
const filteredNodes = nodes.filter((node) => connectedNodeIds.has(node.id));
|
||||
|
||||
return { nodes: filteredNodes, links: filteredLinks };
|
||||
}
|
||||
|
||||
|
||||
const UserStats = (props: { data: UserAnalysisResponse }) => {
|
||||
const graphData = useMemo(() => ApiToGraphData(props.data.interaction_graph), [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 });
|
||||
|
||||
@@ -63,86 +68,113 @@ const UserStats = (props: { data: UserAnalysisResponse }) => {
|
||||
|
||||
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 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 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 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]");
|
||||
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="Replies"
|
||||
value={totalInteractions.toLocaleString()}
|
||||
sublabel="Links with at least 2 replies"
|
||||
style={{ gridColumn: "span 3" }}
|
||||
/>
|
||||
<Card
|
||||
label="Replies per Connected User"
|
||||
value={avgInteractionsPerConnectedUser.toFixed(1)}
|
||||
sublabel="Average from visible graph links"
|
||||
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" }}
|
||||
/>
|
||||
<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="Replies"
|
||||
value={totalInteractions.toLocaleString()}
|
||||
sublabel="Links with at least 2 replies"
|
||||
style={{ gridColumn: "span 3" }}
|
||||
/>
|
||||
<Card
|
||||
label="Replies per Connected User"
|
||||
value={avgInteractionsPerConnectedUser.toFixed(1)}
|
||||
sublabel="Average from visible graph links"
|
||||
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 User Link"
|
||||
value={strongestLink ? `${strongestLink.source} -> ${strongestLink.target}` : "—"}
|
||||
sublabel={strongestLink ? `${strongestLink.value.toLocaleString()} replies` : "No graph links after filtering"}
|
||||
style={{ gridColumn: "span 6" }}
|
||||
/>
|
||||
<Card
|
||||
label="Most Comment-Heavy User"
|
||||
value={highlyInteractiveUser?.author ?? "—"}
|
||||
sublabel={
|
||||
highlyInteractiveUser
|
||||
? `${Math.round(highlyInteractiveUser.comment_share * 100)}% comments`
|
||||
: "No user distribution available"
|
||||
}
|
||||
style={{ gridColumn: "span 6" }}
|
||||
/>
|
||||
<Card
|
||||
label="Strongest User Link"
|
||||
value={
|
||||
strongestLink
|
||||
? `${strongestLink.source} -> ${strongestLink.target}`
|
||||
: "—"
|
||||
}
|
||||
sublabel={
|
||||
strongestLink
|
||||
? `${strongestLink.value.toLocaleString()} replies`
|
||||
: "No graph links after filtering"
|
||||
}
|
||||
style={{ gridColumn: "span 6" }}
|
||||
/>
|
||||
<Card
|
||||
label="Most Comment-Heavy 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}>
|
||||
Each node is a user, and each link shows replies between them.
|
||||
</p>
|
||||
<div ref={graphContainerRef} style={{ width: "100%", height: graphSize.height }}>
|
||||
<ForceGraph3D
|
||||
width={graphSize.width}
|
||||
height={graphSize.height}
|
||||
graphData={graphData}
|
||||
nodeAutoColorBy="id"
|
||||
linkDirectionalParticles={1}
|
||||
linkDirectionalParticleSpeed={0.004}
|
||||
linkWidth={(link) => Math.sqrt(Number(link.value))}
|
||||
nodeLabel={(node) => `${node.id}`}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
||||
<h2 style={styles.sectionTitle}>User Interaction Graph</h2>
|
||||
<p style={styles.sectionSubtitle}>
|
||||
Each node is a user, and each link shows replies between them.
|
||||
</p>
|
||||
<div
|
||||
ref={graphContainerRef}
|
||||
style={{ width: "100%", height: graphSize.height }}
|
||||
>
|
||||
<ForceGraph3D
|
||||
width={graphSize.width}
|
||||
height={graphSize.height}
|
||||
graphData={graphData}
|
||||
nodeAutoColorBy="id"
|
||||
linkDirectionalParticles={1}
|
||||
linkDirectionalParticleSpeed={0.004}
|
||||
linkWidth={(link) => Math.sqrt(Number(link.value))}
|
||||
nodeLabel={(node) => `${node.id}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default UserStats;
|
||||
|
||||
Reference in New Issue
Block a user