perf(stats): memoize derived state and reduce intermediate allocations
This commit is contained in:
@@ -1,18 +1,18 @@
|
|||||||
import type { ContentAnalysisResponse } from "../types/ApiTypes";
|
import type { EmotionalAnalysisResponse } from "../types/ApiTypes";
|
||||||
import StatsStyling from "../styles/stats_styling";
|
import StatsStyling from "../styles/stats_styling";
|
||||||
|
|
||||||
const styles = StatsStyling;
|
const styles = StatsStyling;
|
||||||
|
|
||||||
type EmotionalStatsProps = {
|
type EmotionalStatsProps = {
|
||||||
contentData: ContentAnalysisResponse;
|
emotionalData: EmotionalAnalysisResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmotionalStats = ({ contentData }: EmotionalStatsProps) => {
|
const EmotionalStats = ({ emotionalData }: EmotionalStatsProps) => {
|
||||||
const rows = contentData.average_emotion_by_topic ?? [];
|
const rows = emotionalData.average_emotion_by_topic ?? [];
|
||||||
const overallEmotionAverage = contentData.overall_emotion_average ?? [];
|
const overallEmotionAverage = emotionalData.overall_emotion_average ?? [];
|
||||||
const dominantEmotionDistribution =
|
const dominantEmotionDistribution =
|
||||||
contentData.dominant_emotion_distribution ?? [];
|
emotionalData.dominant_emotion_distribution ?? [];
|
||||||
const emotionBySource = contentData.emotion_by_source ?? [];
|
const emotionBySource = emotionalData.emotion_by_source ?? [];
|
||||||
const lowSampleThreshold = 20;
|
const lowSampleThreshold = 20;
|
||||||
const stableSampleThreshold = 50;
|
const stableSampleThreshold = 50;
|
||||||
const emotionKeys = rows.length
|
const emotionKeys = rows.length
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ type InteractionalStatsProps = {
|
|||||||
const InteractionalStats = ({ data }: InteractionalStatsProps) => {
|
const InteractionalStats = ({ data }: InteractionalStatsProps) => {
|
||||||
const graph = data.interaction_graph ?? {};
|
const graph = data.interaction_graph ?? {};
|
||||||
const userCount = Object.keys(graph).length;
|
const userCount = Object.keys(graph).length;
|
||||||
const edges = Object.values(graph).flatMap((targets) =>
|
let edgeCount = 0;
|
||||||
Object.values(targets),
|
let interactionVolume = 0;
|
||||||
);
|
for (const targets of Object.values(graph)) {
|
||||||
const edgeCount = edges.length;
|
for (const value of Object.values(targets)) {
|
||||||
const interactionVolume = edges.reduce((sum, value) => sum + value, 0);
|
edgeCount += 1;
|
||||||
|
interactionVolume += value;
|
||||||
|
}
|
||||||
|
}
|
||||||
const concentration = data.conversation_concentration;
|
const concentration = data.conversation_concentration;
|
||||||
const topTenCommentShare =
|
const topTenCommentShare =
|
||||||
typeof concentration?.top_10pct_comment_share === "number"
|
typeof concentration?.top_10pct_comment_share === "number"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { memo, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LineChart,
|
LineChart,
|
||||||
Line,
|
Line,
|
||||||
@@ -18,21 +18,37 @@ import UserModal from "../components/UserModal";
|
|||||||
import {
|
import {
|
||||||
type SummaryResponse,
|
type SummaryResponse,
|
||||||
type FrequencyWord,
|
type FrequencyWord,
|
||||||
type UserAnalysisResponse,
|
type UserEndpointResponse,
|
||||||
type TimeAnalysisResponse,
|
type TimeAnalysisResponse,
|
||||||
type ContentAnalysisResponse,
|
type LinguisticAnalysisResponse,
|
||||||
type User,
|
type User,
|
||||||
} from "../types/ApiTypes";
|
} from "../types/ApiTypes";
|
||||||
|
|
||||||
const styles = StatsStyling;
|
const styles = StatsStyling;
|
||||||
|
const MAX_WORDCLOUD_WORDS = 250;
|
||||||
|
|
||||||
|
const WORDCLOUD_OPTIONS = {
|
||||||
|
rotations: 2,
|
||||||
|
rotationAngles: [0, 90] as [number, number],
|
||||||
|
fontSizes: [14, 60] as [number, number],
|
||||||
|
enableTooltip: true,
|
||||||
|
};
|
||||||
|
|
||||||
type SummaryStatsProps = {
|
type SummaryStatsProps = {
|
||||||
userData: UserAnalysisResponse | null;
|
userData: UserEndpointResponse | null;
|
||||||
timeData: TimeAnalysisResponse | null;
|
timeData: TimeAnalysisResponse | null;
|
||||||
contentData: ContentAnalysisResponse | null;
|
linguisticData: LinguisticAnalysisResponse | null;
|
||||||
summary: SummaryResponse | null;
|
summary: SummaryResponse | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type WordCloudPanelProps = {
|
||||||
|
words: { text: string; value: number }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const WordCloudPanel = memo(({ words }: WordCloudPanelProps) => (
|
||||||
|
<ReactWordcloud words={words} options={WORDCLOUD_OPTIONS} />
|
||||||
|
));
|
||||||
|
|
||||||
function formatDateRange(startUnix: number, endUnix: number) {
|
function formatDateRange(startUnix: number, endUnix: number) {
|
||||||
const start = new Date(startUnix * 1000);
|
const start = new Date(startUnix * 1000);
|
||||||
const end = new Date(endUnix * 1000);
|
const end = new Date(endUnix * 1000);
|
||||||
@@ -57,12 +73,34 @@ function convertFrequencyData(data: FrequencyWord[]) {
|
|||||||
const SummaryStats = ({
|
const SummaryStats = ({
|
||||||
userData,
|
userData,
|
||||||
timeData,
|
timeData,
|
||||||
contentData,
|
linguisticData,
|
||||||
summary,
|
summary,
|
||||||
}: SummaryStatsProps) => {
|
}: SummaryStatsProps) => {
|
||||||
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
||||||
const selectedUserData: User | null =
|
const usersByAuthor = useMemo(() => {
|
||||||
userData?.users.find((u) => u.author === selectedUser) ?? null;
|
const nextMap = new Map<string, User>();
|
||||||
|
for (const user of userData?.users ?? []) {
|
||||||
|
nextMap.set(user.author, user);
|
||||||
|
}
|
||||||
|
return nextMap;
|
||||||
|
}, [userData?.users]);
|
||||||
|
|
||||||
|
const selectedUserData: User | null = selectedUser
|
||||||
|
? usersByAuthor.get(selectedUser) ?? null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const wordCloudWords = useMemo(
|
||||||
|
() =>
|
||||||
|
convertFrequencyData(
|
||||||
|
(linguisticData?.word_frequencies ?? []).slice(0, MAX_WORDCLOUD_WORDS),
|
||||||
|
),
|
||||||
|
[linguisticData?.word_frequencies],
|
||||||
|
);
|
||||||
|
|
||||||
|
const topUsersPreview = useMemo(
|
||||||
|
() => (userData?.top_users ?? []).slice(0, 100),
|
||||||
|
[userData?.top_users],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
@@ -152,7 +190,12 @@ const SummaryStats = ({
|
|||||||
<XAxis dataKey="date" />
|
<XAxis dataKey="date" />
|
||||||
<YAxis />
|
<YAxis />
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
<Line type="monotone" dataKey="count" name="Events" />
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="count"
|
||||||
|
name="Events"
|
||||||
|
isAnimationActive={false}
|
||||||
|
/>
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,15 +209,7 @@ const SummaryStats = ({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={styles.chartWrapper}>
|
<div style={styles.chartWrapper}>
|
||||||
<ReactWordcloud
|
<WordCloudPanel words={wordCloudWords} />
|
||||||
words={convertFrequencyData(contentData?.word_frequencies ?? [])}
|
|
||||||
options={{
|
|
||||||
rotations: 2,
|
|
||||||
rotationAngles: [0, 90],
|
|
||||||
fontSizes: [14, 60],
|
|
||||||
enableTooltip: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,7 +221,7 @@ const SummaryStats = ({
|
|||||||
<p style={styles.sectionSubtitle}>Who posted the most events.</p>
|
<p style={styles.sectionSubtitle}>Who posted the most events.</p>
|
||||||
|
|
||||||
<div style={styles.topUsersList}>
|
<div style={styles.topUsersList}>
|
||||||
{userData?.top_users.slice(0, 100).map((item) => (
|
{topUsersPreview.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={`${item.author}-${item.source}`}
|
key={`${item.author}-${item.source}`}
|
||||||
style={{ ...styles.topUserItem, cursor: "pointer" }}
|
style={{ ...styles.topUserItem, cursor: "pointer" }}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import ForceGraph3D from "react-force-graph-3d";
|
import ForceGraph3D from "react-force-graph-3d";
|
||||||
|
|
||||||
import {
|
import { type TopUser, type InteractionGraph } from "../types/ApiTypes";
|
||||||
type UserAnalysisResponse,
|
|
||||||
type InteractionGraph,
|
|
||||||
} from "../types/ApiTypes";
|
|
||||||
|
|
||||||
import StatsStyling from "../styles/stats_styling";
|
import StatsStyling from "../styles/stats_styling";
|
||||||
import Card from "./Card";
|
import Card from "./Card";
|
||||||
@@ -18,36 +15,41 @@ type GraphLink = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function ApiToGraphData(apiData: InteractionGraph) {
|
function ApiToGraphData(apiData: InteractionGraph) {
|
||||||
const nodes = Object.keys(apiData).map((username) => ({ id: username }));
|
|
||||||
const links: GraphLink[] = [];
|
const links: GraphLink[] = [];
|
||||||
|
const connectedNodeIds = new Set<string>();
|
||||||
|
|
||||||
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)) {
|
||||||
|
if (count < 2 || source === "[deleted]" || target === "[deleted]") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
links.push({ source, target, value: count });
|
links.push({ source, target, value: count });
|
||||||
|
connectedNodeIds.add(source);
|
||||||
|
connectedNodeIds.add(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// drop low-value and deleted interactions to reduce clutter
|
const filteredNodes = Array.from(connectedNodeIds, (id) => ({ id }));
|
||||||
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
|
return { nodes: filteredNodes, links };
|
||||||
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 }) => {
|
type UserStatsProps = {
|
||||||
|
topUsers: TopUser[];
|
||||||
|
interactionGraph: InteractionGraph;
|
||||||
|
totalUsers: number;
|
||||||
|
mostCommentHeavyUser: { author: string; commentShare: number } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserStats = ({
|
||||||
|
topUsers,
|
||||||
|
interactionGraph,
|
||||||
|
totalUsers,
|
||||||
|
mostCommentHeavyUser,
|
||||||
|
}: UserStatsProps) => {
|
||||||
const graphData = useMemo(
|
const graphData = useMemo(
|
||||||
() => ApiToGraphData(props.data.interaction_graph),
|
() => ApiToGraphData(interactionGraph),
|
||||||
[props.data.interaction_graph],
|
[interactionGraph],
|
||||||
);
|
);
|
||||||
const graphContainerRef = useRef<HTMLDivElement | null>(null);
|
const graphContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [graphSize, setGraphSize] = useState({ width: 720, height: 540 });
|
const [graphSize, setGraphSize] = useState({ width: 720, height: 540 });
|
||||||
@@ -66,7 +68,6 @@ const UserStats = (props: { data: UserAnalysisResponse }) => {
|
|||||||
return () => window.removeEventListener("resize", updateGraphSize);
|
return () => window.removeEventListener("resize", updateGraphSize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const totalUsers = props.data.users.length;
|
|
||||||
const connectedUsers = graphData.nodes.length;
|
const connectedUsers = graphData.nodes.length;
|
||||||
const totalInteractions = graphData.links.reduce(
|
const totalInteractions = graphData.links.reduce(
|
||||||
(sum, link) => sum + link.value,
|
(sum, link) => sum + link.value,
|
||||||
@@ -86,11 +87,7 @@ const UserStats = (props: { data: UserAnalysisResponse }) => {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const highlyInteractiveUser = [...props.data.users].sort(
|
const mostActiveUser = topUsers.find(
|
||||||
(a, b) => b.comment_share - a.comment_share,
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
const mostActiveUser = props.data.top_users.find(
|
|
||||||
(u) => u.author !== "[deleted]",
|
(u) => u.author !== "[deleted]",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -142,10 +139,10 @@ const UserStats = (props: { data: UserAnalysisResponse }) => {
|
|||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Most Comment-Heavy User"
|
label="Most Comment-Heavy User"
|
||||||
value={highlyInteractiveUser?.author ?? "—"}
|
value={mostCommentHeavyUser?.author ?? "—"}
|
||||||
sublabel={
|
sublabel={
|
||||||
highlyInteractiveUser
|
mostCommentHeavyUser
|
||||||
? `${Math.round(highlyInteractiveUser.comment_share * 100)}% comments`
|
? `${Math.round(mostCommentHeavyUser.commentShare * 100)}% comments`
|
||||||
: "No user distribution available"
|
: "No user distribution available"
|
||||||
}
|
}
|
||||||
style={{ gridColumn: "span 6" }}
|
style={{ gridColumn: "span 6" }}
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ import CulturalStats from "../components/CulturalStats";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type SummaryResponse,
|
type SummaryResponse,
|
||||||
type UserAnalysisResponse,
|
|
||||||
type TimeAnalysisResponse,
|
type TimeAnalysisResponse,
|
||||||
type ContentAnalysisResponse,
|
type User,
|
||||||
type UserEndpointResponse,
|
type UserEndpointResponse,
|
||||||
type LinguisticAnalysisResponse,
|
type LinguisticAnalysisResponse,
|
||||||
type EmotionalAnalysisResponse,
|
type EmotionalAnalysisResponse,
|
||||||
@@ -28,30 +27,40 @@ const DELETED_USERS = ["[deleted]"];
|
|||||||
const isDeletedUser = (value: string | null | undefined) =>
|
const isDeletedUser = (value: string | null | undefined) =>
|
||||||
DELETED_USERS.includes((value ?? "").trim().toLowerCase());
|
DELETED_USERS.includes((value ?? "").trim().toLowerCase());
|
||||||
|
|
||||||
|
type ActiveView =
|
||||||
|
| "summary"
|
||||||
|
| "emotional"
|
||||||
|
| "user"
|
||||||
|
| "linguistic"
|
||||||
|
| "interactional"
|
||||||
|
| "cultural";
|
||||||
|
|
||||||
|
type UserStatsMeta = {
|
||||||
|
totalUsers: number;
|
||||||
|
mostCommentHeavyUser: { author: string; commentShare: number } | null;
|
||||||
|
};
|
||||||
|
|
||||||
const StatPage = () => {
|
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<
|
const [activeView, setActiveView] = useState<ActiveView>("summary");
|
||||||
| "summary"
|
|
||||||
| "emotional"
|
|
||||||
| "user"
|
|
||||||
| "linguistic"
|
|
||||||
| "interactional"
|
|
||||||
| "cultural"
|
|
||||||
>("summary");
|
|
||||||
|
|
||||||
const [userData, setUserData] = useState<UserAnalysisResponse | null>(null);
|
const [userData, setUserData] = useState<UserEndpointResponse | null>(null);
|
||||||
const [timeData, setTimeData] = useState<TimeAnalysisResponse | null>(null);
|
const [timeData, setTimeData] = useState<TimeAnalysisResponse | null>(null);
|
||||||
const [contentData, setContentData] =
|
|
||||||
useState<ContentAnalysisResponse | null>(null);
|
|
||||||
const [linguisticData, setLinguisticData] =
|
const [linguisticData, setLinguisticData] =
|
||||||
useState<LinguisticAnalysisResponse | null>(null);
|
useState<LinguisticAnalysisResponse | null>(null);
|
||||||
|
const [emotionalData, setEmotionalData] =
|
||||||
|
useState<EmotionalAnalysisResponse | null>(null);
|
||||||
const [interactionData, setInteractionData] =
|
const [interactionData, setInteractionData] =
|
||||||
useState<InteractionAnalysisResponse | null>(null);
|
useState<InteractionAnalysisResponse | null>(null);
|
||||||
const [culturalData, setCulturalData] =
|
const [culturalData, setCulturalData] =
|
||||||
useState<CulturalAnalysisResponse | null>(null);
|
useState<CulturalAnalysisResponse | null>(null);
|
||||||
const [summary, setSummary] = useState<SummaryResponse | null>(null);
|
const [summary, setSummary] = useState<SummaryResponse | null>(null);
|
||||||
|
const [userStatsMeta, setUserStatsMeta] = useState<UserStatsMeta>({
|
||||||
|
totalUsers: 0,
|
||||||
|
mostCommentHeavyUser: null,
|
||||||
|
});
|
||||||
|
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const beforeDateRef = useRef<HTMLInputElement>(null);
|
const beforeDateRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -185,14 +194,35 @@ const StatPage = () => {
|
|||||||
|
|
||||||
const filteredTopUsers: typeof topUsersList = [];
|
const filteredTopUsers: typeof topUsersList = [];
|
||||||
for (const user of topUsersList) {
|
for (const user of topUsersList) {
|
||||||
if (isDeletedUser(user.author)) continue;
|
if (isDeletedUser(user.author)) continue;
|
||||||
filteredTopUsers.push(user);
|
filteredTopUsers.push(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredInteractionGraph: Record<
|
let mostCommentHeavyUser: UserStatsMeta["mostCommentHeavyUser"] =
|
||||||
string,
|
null;
|
||||||
Record<string, number>
|
for (const user of filteredUsers) {
|
||||||
> = {};
|
const currentShare = user.comment_share ?? 0;
|
||||||
|
if (
|
||||||
|
!mostCommentHeavyUser ||
|
||||||
|
currentShare > mostCommentHeavyUser.commentShare
|
||||||
|
) {
|
||||||
|
mostCommentHeavyUser = {
|
||||||
|
author: user.author,
|
||||||
|
commentShare: currentShare,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const topAuthors = new Set(filteredTopUsers.map((entry) => entry.author));
|
||||||
|
const summaryUsers: User[] = [];
|
||||||
|
for (const user of filteredUsers) {
|
||||||
|
if (topAuthors.has(user.author)) {
|
||||||
|
summaryUsers.push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredInteractionGraph: Record<string, Record<string, number>> =
|
||||||
|
{};
|
||||||
for (const [source, targets] of Object.entries(interactionGraphRaw)) {
|
for (const [source, targets] of Object.entries(interactionGraphRaw)) {
|
||||||
if (isDeletedUser(source)) {
|
if (isDeletedUser(source)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -220,16 +250,9 @@ const StatPage = () => {
|
|||||||
filteredTopInteractionPairs.push(pairEntry);
|
filteredTopInteractionPairs.push(pairEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
const combinedUserData: UserAnalysisResponse = {
|
const filteredUserData: UserEndpointResponse = {
|
||||||
...userRes.data,
|
users: summaryUsers,
|
||||||
users: filteredUsers,
|
|
||||||
top_users: filteredTopUsers,
|
top_users: filteredTopUsers,
|
||||||
interaction_graph: filteredInteractionGraph,
|
|
||||||
};
|
|
||||||
|
|
||||||
const combinedContentData: ContentAnalysisResponse = {
|
|
||||||
...linguisticRes.data,
|
|
||||||
...emotionalRes.data,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredInteractionData: InteractionAnalysisResponse = {
|
const filteredInteractionData: InteractionAnalysisResponse = {
|
||||||
@@ -243,10 +266,14 @@ const StatPage = () => {
|
|||||||
unique_users: filteredUsers.length,
|
unique_users: filteredUsers.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
setUserData(combinedUserData);
|
setUserData(filteredUserData);
|
||||||
|
setUserStatsMeta({
|
||||||
|
totalUsers: filteredUsers.length,
|
||||||
|
mostCommentHeavyUser,
|
||||||
|
});
|
||||||
setTimeData(timeRes.data || null);
|
setTimeData(timeRes.data || null);
|
||||||
setContentData(combinedContentData);
|
|
||||||
setLinguisticData(linguisticRes.data || null);
|
setLinguisticData(linguisticRes.data || null);
|
||||||
|
setEmotionalData(emotionalRes.data || null);
|
||||||
setInteractionData(filteredInteractionData || null);
|
setInteractionData(filteredInteractionData || null);
|
||||||
setCulturalData(culturalRes.data || null);
|
setCulturalData(culturalRes.data || null);
|
||||||
setSummary(filteredSummary || null);
|
setSummary(filteredSummary || null);
|
||||||
@@ -435,22 +462,35 @@ const StatPage = () => {
|
|||||||
<SummaryStats
|
<SummaryStats
|
||||||
userData={userData}
|
userData={userData}
|
||||||
timeData={timeData}
|
timeData={timeData}
|
||||||
contentData={contentData}
|
linguisticData={linguisticData}
|
||||||
summary={summary}
|
summary={summary}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeView === "emotional" && contentData && (
|
{activeView === "emotional" && emotionalData && (
|
||||||
<EmotionalStats contentData={contentData} />
|
<EmotionalStats emotionalData={emotionalData} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeView === "emotional" && !contentData && (
|
{activeView === "emotional" && !emotionalData && (
|
||||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||||
No emotional data available.
|
No emotional data available.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeView === "user" && userData && <UserStats data={userData} />}
|
{activeView === "user" && userData && interactionData && (
|
||||||
|
<UserStats
|
||||||
|
topUsers={userData.top_users}
|
||||||
|
interactionGraph={interactionData.interaction_graph}
|
||||||
|
totalUsers={userStatsMeta.totalUsers}
|
||||||
|
mostCommentHeavyUser={userStatsMeta.mostCommentHeavyUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeView === "user" && (!userData || !interactionData) && (
|
||||||
|
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||||
|
No user network data available.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{activeView === "linguistic" && linguisticData && (
|
{activeView === "linguistic" && linguisticData && (
|
||||||
<LinguisticStats data={linguisticData} />
|
<LinguisticStats data={linguisticData} />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ResponsiveHeatMap } from "@nivo/heatmap";
|
import { ResponsiveHeatMap } from "@nivo/heatmap";
|
||||||
|
import { memo, useMemo } from "react";
|
||||||
|
|
||||||
type ApiRow = Record<number, number>;
|
type ApiRow = Record<number, number>;
|
||||||
type ActivityHeatmapProps = {
|
type ActivityHeatmapProps = {
|
||||||
@@ -40,11 +41,19 @@ const convertWeeklyData = (dataset: ApiRow[]): ChartSeries[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ActivityHeatmap = ({ data }: ActivityHeatmapProps) => {
|
const ActivityHeatmap = ({ data }: ActivityHeatmapProps) => {
|
||||||
const convertedData = convertWeeklyData(data);
|
const convertedData = useMemo(() => convertWeeklyData(data), [data]);
|
||||||
|
|
||||||
const maxValue = Math.max(
|
const maxValue = useMemo(() => {
|
||||||
...convertedData.flatMap((day) => day.data.map((point) => point.y)),
|
let max = 0;
|
||||||
);
|
for (const day of convertedData) {
|
||||||
|
for (const point of day.data) {
|
||||||
|
if (point.y > max) {
|
||||||
|
max = point.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max;
|
||||||
|
}, [convertedData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveHeatMap
|
<ResponsiveHeatMap
|
||||||
@@ -64,4 +73,4 @@ const ActivityHeatmap = ({ data }: ActivityHeatmapProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ActivityHeatmap;
|
export default memo(ActivityHeatmap);
|
||||||
|
|||||||
Reference in New Issue
Block a user