Compare commits
3 Commits
f604fcc531
...
e5414befa7
| Author | SHA1 | Date | |
|---|---|---|---|
| e5414befa7 | |||
| 86926898ce | |||
| b1177540a1 |
@@ -9,6 +9,9 @@ type EmotionalStatsProps = {
|
|||||||
|
|
||||||
const EmotionalStats = ({contentData}: EmotionalStatsProps) => {
|
const EmotionalStats = ({contentData}: EmotionalStatsProps) => {
|
||||||
const rows = contentData.average_emotion_by_topic ?? [];
|
const rows = contentData.average_emotion_by_topic ?? [];
|
||||||
|
const overallEmotionAverage = contentData.overall_emotion_average ?? [];
|
||||||
|
const dominantEmotionDistribution = contentData.dominant_emotion_distribution ?? [];
|
||||||
|
const emotionBySource = contentData.emotion_by_source ?? [];
|
||||||
const lowSampleThreshold = 20;
|
const lowSampleThreshold = 20;
|
||||||
const stableSampleThreshold = 50;
|
const stableSampleThreshold = 50;
|
||||||
const emotionKeys = rows.length
|
const emotionKeys = rows.length
|
||||||
@@ -64,39 +67,104 @@ const EmotionalStats = ({contentData}: EmotionalStatsProps) => {
|
|||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||||
<h2 style={styles.sectionTitle}>Average Emotion by Topic</h2>
|
<h2 style={styles.sectionTitle}>Topic Mood Overview</h2>
|
||||||
<p style={styles.sectionSubtitle}>Read confidence together with sample size. Topics with fewer than {lowSampleThreshold} events are usually noisy and less reliable.</p>
|
<p style={styles.sectionSubtitle}>Use the strength score together with post count. Topics with fewer than {lowSampleThreshold} events are often noisy.</p>
|
||||||
<div style={styles.emotionalSummaryRow}>
|
<div style={styles.emotionalSummaryRow}>
|
||||||
<span><strong style={{ color: "#24292f" }}>Topics:</strong> {strongestPerTopic.length}</span>
|
<span><strong style={{ color: "#24292f" }}>Topics:</strong> {strongestPerTopic.length}</span>
|
||||||
<span><strong style={{ color: "#24292f" }}>Median Sample:</strong> {medianSampleSize} events</span>
|
<span><strong style={{ color: "#24292f" }}>Median Posts:</strong> {medianSampleSize}</span>
|
||||||
<span><strong style={{ color: "#24292f" }}>Low Sample (<{lowSampleThreshold}):</strong> {lowSampleTopics}</span>
|
<span><strong style={{ color: "#24292f" }}>Small Topics (<{lowSampleThreshold}):</strong> {lowSampleTopics}</span>
|
||||||
<span><strong style={{ color: "#24292f" }}>Stable Sample ({stableSampleThreshold}+):</strong> {stableSampleTopics}</span>
|
<span><strong style={{ color: "#24292f" }}>Stable Topics ({stableSampleThreshold}+):</strong> {stableSampleTopics}</span>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ ...styles.sectionSubtitle, marginTop: 10, marginBottom: 0 }}>
|
<p style={{ ...styles.sectionSubtitle, marginTop: 10, marginBottom: 0 }}>
|
||||||
Confidence reflects how strongly one emotion leads within a topic, not model accuracy. Use larger samples for stronger conclusions.
|
Strength means how far the top emotion is ahead in that topic. It does not mean model accuracy.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ ...styles.container, ...styles.grid }}>
|
<div style={{ ...styles.container, ...styles.grid }}>
|
||||||
{strongestPerTopic.map((topic) => (
|
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
||||||
<div key={topic.topic} style={{ ...styles.card, gridColumn: "span 4" }}>
|
<h2 style={styles.sectionTitle}>Mood Averages</h2>
|
||||||
<h3 style={{ ...styles.sectionTitle, marginBottom: 6 }}>{topic.topic}</h3>
|
<p style={styles.sectionSubtitle}>Average score for each emotion.</p>
|
||||||
<div style={styles.emotionalTopicLabel}>
|
{!overallEmotionAverage.length ? (
|
||||||
Top Emotion
|
<div style={styles.topUserMeta}>No overall emotion averages available.</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ ...styles.topUsersList, maxHeight: 260, overflowY: "auto" }}>
|
||||||
|
{[...overallEmotionAverage]
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.map((row) => (
|
||||||
|
<div key={row.emotion} style={styles.topUserItem}>
|
||||||
|
<div style={styles.topUserName}>{formatEmotion(row.emotion)}</div>
|
||||||
|
<div style={styles.topUserMeta}>{row.score.toFixed(3)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.emotionalTopicValue}>
|
)}
|
||||||
{formatEmotion(topic.emotion)}
|
</div>
|
||||||
|
|
||||||
|
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
||||||
|
<h2 style={styles.sectionTitle}>Mood Split</h2>
|
||||||
|
<p style={styles.sectionSubtitle}>How often each emotion is dominant.</p>
|
||||||
|
{!dominantEmotionDistribution.length ? (
|
||||||
|
<div style={styles.topUserMeta}>No dominant-emotion split available.</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ ...styles.topUsersList, maxHeight: 260, overflowY: "auto" }}>
|
||||||
|
{[...dominantEmotionDistribution]
|
||||||
|
.sort((a, b) => b.ratio - a.ratio)
|
||||||
|
.map((row) => (
|
||||||
|
<div key={row.emotion} style={styles.topUserItem}>
|
||||||
|
<div style={styles.topUserName}>{formatEmotion(row.emotion)}</div>
|
||||||
|
<div style={styles.topUserMeta}>{(row.ratio * 100).toFixed(1)}% • {row.count.toLocaleString()} events</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.emotionalMetricRow}>
|
)}
|
||||||
<span>Confidence</span>
|
</div>
|
||||||
<span style={styles.emotionalMetricValue}>{topic.value.toFixed(3)}</span>
|
|
||||||
</div>
|
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
||||||
<div style={styles.emotionalMetricRowCompact}>
|
<h2 style={styles.sectionTitle}>Mood by Source</h2>
|
||||||
<span>Sample Size</span>
|
<p style={styles.sectionSubtitle}>Leading emotion in each source.</p>
|
||||||
<span style={styles.emotionalMetricValue}>{topic.count} events</span>
|
{!emotionBySource.length ? (
|
||||||
|
<div style={styles.topUserMeta}>No source emotion profile available.</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ ...styles.topUsersList, maxHeight: 260, overflowY: "auto" }}>
|
||||||
|
{[...emotionBySource]
|
||||||
|
.sort((a, b) => b.event_count - a.event_count)
|
||||||
|
.map((row) => (
|
||||||
|
<div key={row.source} style={styles.topUserItem}>
|
||||||
|
<div style={styles.topUserName}>{row.source}</div>
|
||||||
|
<div style={styles.topUserMeta}>
|
||||||
|
{formatEmotion(row.dominant_emotion)} • {row.dominant_score.toFixed(3)} • {row.event_count.toLocaleString()} events
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
||||||
|
<h2 style={styles.sectionTitle}>Topic Snapshots</h2>
|
||||||
|
<p style={styles.sectionSubtitle}>Per-topic mood with strength and post count.</p>
|
||||||
|
<div style={{ ...styles.grid, marginTop: 10 }}>
|
||||||
|
{strongestPerTopic.map((topic) => (
|
||||||
|
<div key={topic.topic} style={{ ...styles.cardBase, gridColumn: "span 4" }}>
|
||||||
|
<h3 style={{ ...styles.sectionTitle, marginBottom: 6 }}>{topic.topic}</h3>
|
||||||
|
<div style={styles.emotionalTopicLabel}>
|
||||||
|
Likely Mood
|
||||||
|
</div>
|
||||||
|
<div style={styles.emotionalTopicValue}>
|
||||||
|
{formatEmotion(topic.emotion)}
|
||||||
|
</div>
|
||||||
|
<div style={styles.emotionalMetricRow}>
|
||||||
|
<span>Strength</span>
|
||||||
|
<span style={styles.emotionalMetricValue}>{topic.value.toFixed(3)}</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.emotionalMetricRowCompact}>
|
||||||
|
<span>Posts in Topic</span>
|
||||||
|
<span style={styles.emotionalMetricValue}>{topic.count}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ const InteractionalStats = ({ data }: InteractionalStatsProps) => {
|
|||||||
const singleCommentAuthorRatio = typeof concentration?.single_comment_author_ratio === "number"
|
const singleCommentAuthorRatio = typeof concentration?.single_comment_author_ratio === "number"
|
||||||
? concentration.single_comment_author_ratio
|
? concentration.single_comment_author_ratio
|
||||||
: null;
|
: null;
|
||||||
|
const singleCommentAuthors = typeof concentration?.single_comment_authors === "number"
|
||||||
|
? concentration.single_comment_authors
|
||||||
|
: null;
|
||||||
|
|
||||||
const topPairs = (data.top_interaction_pairs ?? [])
|
const topPairs = (data.top_interaction_pairs ?? [])
|
||||||
.filter((item): item is [[string, string], number] => {
|
.filter((item): item is [[string, string], number] => {
|
||||||
@@ -84,48 +87,55 @@ const InteractionalStats = ({ data }: InteractionalStatsProps) => {
|
|||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
<div style={{ ...styles.container, ...styles.grid }}>
|
<div style={{ ...styles.container, ...styles.grid }}>
|
||||||
|
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
||||||
|
<h2 style={styles.sectionTitle}>Conversation Overview</h2>
|
||||||
|
<p style={styles.sectionSubtitle}>Who talks to who, and how concentrated the replies are.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
label="Avg Thread Depth"
|
label="Average Reply Depth"
|
||||||
value={typeof data.average_thread_depth === "number" ? data.average_thread_depth.toFixed(2) : "—"}
|
value={typeof data.average_thread_depth === "number" ? data.average_thread_depth.toFixed(2) : "—"}
|
||||||
sublabel="Depth from reply chains"
|
sublabel="How deep reply chains usually go"
|
||||||
style={{ gridColumn: "span 3" }}
|
style={{ gridColumn: "span 3" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Network Users"
|
label="Users in Network"
|
||||||
value={userCount.toLocaleString()}
|
value={userCount.toLocaleString()}
|
||||||
sublabel="Authors in interaction graph"
|
sublabel="Users in the reply graph"
|
||||||
style={{ gridColumn: "span 3" }}
|
style={{ gridColumn: "span 3" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Unique Links"
|
label="User-to-User Links"
|
||||||
value={edgeCount.toLocaleString()}
|
value={edgeCount.toLocaleString()}
|
||||||
sublabel="Directed source-target pairs"
|
sublabel="Unique reply directions"
|
||||||
style={{ gridColumn: "span 3" }}
|
style={{ gridColumn: "span 3" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Interaction Volume"
|
label="Total Replies"
|
||||||
value={interactionVolume.toLocaleString()}
|
value={interactionVolume.toLocaleString()}
|
||||||
sublabel="Sum of link weights"
|
sublabel="All reply links combined"
|
||||||
style={{ gridColumn: "span 3" }}
|
style={{ gridColumn: "span 3" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Top 10% Comment Share"
|
label="Concentrated Replies"
|
||||||
value={topTenSharePercent === null ? "-" : `${topTenSharePercent.toFixed(1)}%`}
|
value={topTenSharePercent === null ? "-" : `${topTenSharePercent.toFixed(1)}%`}
|
||||||
sublabel={topTenAuthorCount === null || totalCommentingAuthors === null
|
sublabel={topTenAuthorCount === null || totalCommentingAuthors === null
|
||||||
? "Comment volume held by top commenters"
|
? "Reply share from the top 10% commenters"
|
||||||
: `${topTenAuthorCount.toLocaleString()} of ${totalCommentingAuthors.toLocaleString()} authors`}
|
: `${topTenAuthorCount.toLocaleString()} of ${totalCommentingAuthors.toLocaleString()} authors`}
|
||||||
style={{ gridColumn: "span 6" }}
|
style={{ gridColumn: "span 6" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Single-Comment Authors"
|
label="Single-Comment Authors"
|
||||||
value={singleCommentAuthorRatio === null ? "-" : `${(singleCommentAuthorRatio * 100).toFixed(1)}%`}
|
value={singleCommentAuthorRatio === null ? "-" : `${(singleCommentAuthorRatio * 100).toFixed(1)}%`}
|
||||||
sublabel="Authors who commented exactly once"
|
sublabel={singleCommentAuthors === null
|
||||||
|
? "Authors who commented exactly once"
|
||||||
|
: `${singleCommentAuthors.toLocaleString()} authors commented exactly once`}
|
||||||
style={{ gridColumn: "span 6" }}
|
style={{ gridColumn: "span 6" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
||||||
<h2 style={styles.sectionTitle}>Interaction Visuals</h2>
|
<h2 style={styles.sectionTitle}>Conversation Visuals</h2>
|
||||||
<p style={styles.sectionSubtitle}>Quick charts for interaction direction and conversation concentration.</p>
|
<p style={styles.sectionSubtitle}>Main reply links and concentration split.</p>
|
||||||
|
|
||||||
<div style={{ ...styles.grid, marginTop: 12 }}>
|
<div style={{ ...styles.grid, marginTop: 12 }}>
|
||||||
<div style={{ ...styles.cardBase, gridColumn: "span 6" }}>
|
<div style={{ ...styles.cardBase, gridColumn: "span 6" }}>
|
||||||
@@ -175,8 +185,8 @@ const InteractionalStats = ({ data }: InteractionalStatsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
||||||
<h2 style={styles.sectionTitle}>Top Interaction Pairs</h2>
|
<h2 style={styles.sectionTitle}>Frequent Reply Paths</h2>
|
||||||
<p style={styles.sectionSubtitle}>Most frequent directed reply paths between users.</p>
|
<p style={styles.sectionSubtitle}>Most common user-to-user reply paths.</p>
|
||||||
{!topPairs.length ? (
|
{!topPairs.length ? (
|
||||||
<div style={styles.topUserMeta}>No interaction pair data available.</div>
|
<div style={styles.topUserMeta}>No interaction pair data available.</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -21,28 +21,33 @@ const LinguisticStats = ({ data }: LinguisticStatsProps) => {
|
|||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
<div style={{ ...styles.container, ...styles.grid }}>
|
<div style={{ ...styles.container, ...styles.grid }}>
|
||||||
|
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
||||||
|
<h2 style={styles.sectionTitle}>Language Overview</h2>
|
||||||
|
<p style={styles.sectionSubtitle}>Quick read on how broad and repetitive the wording is.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
label="Total Tokens"
|
label="Total Words"
|
||||||
value={lexical?.total_tokens?.toLocaleString() ?? "—"}
|
value={lexical?.total_tokens?.toLocaleString() ?? "—"}
|
||||||
sublabel="After token filtering"
|
sublabel="Words after basic filtering"
|
||||||
style={{ gridColumn: "span 4" }}
|
style={{ gridColumn: "span 4" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Unique Tokens"
|
label="Unique Words"
|
||||||
value={lexical?.unique_tokens?.toLocaleString() ?? "—"}
|
value={lexical?.unique_tokens?.toLocaleString() ?? "—"}
|
||||||
sublabel="Distinct vocabulary items"
|
sublabel="Different words used"
|
||||||
style={{ gridColumn: "span 4" }}
|
style={{ gridColumn: "span 4" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Type-Token Ratio"
|
label="Vocabulary Variety"
|
||||||
value={typeof lexical?.ttr === "number" ? lexical.ttr.toFixed(4) : "—"}
|
value={typeof lexical?.ttr === "number" ? lexical.ttr.toFixed(4) : "—"}
|
||||||
sublabel="Vocabulary richness proxy"
|
sublabel="Higher means less repetition"
|
||||||
style={{ gridColumn: "span 4" }}
|
style={{ gridColumn: "span 4" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
||||||
<h2 style={styles.sectionTitle}>Top Words</h2>
|
<h2 style={styles.sectionTitle}>Top Words</h2>
|
||||||
<p style={styles.sectionSubtitle}>Most frequent filtered terms.</p>
|
<p style={styles.sectionSubtitle}>Most used single words.</p>
|
||||||
<div style={{ ...styles.topUsersList, maxHeight: 360, overflowY: "auto" }}>
|
<div style={{ ...styles.topUsersList, maxHeight: 360, overflowY: "auto" }}>
|
||||||
{topWords.map((item) => (
|
{topWords.map((item) => (
|
||||||
<div key={item.word} style={styles.topUserItem}>
|
<div key={item.word} style={styles.topUserItem}>
|
||||||
@@ -55,7 +60,7 @@ const LinguisticStats = ({ data }: LinguisticStatsProps) => {
|
|||||||
|
|
||||||
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
||||||
<h2 style={styles.sectionTitle}>Top Bigrams</h2>
|
<h2 style={styles.sectionTitle}>Top Bigrams</h2>
|
||||||
<p style={styles.sectionSubtitle}>Most frequent 2-word phrases.</p>
|
<p style={styles.sectionSubtitle}>Most used 2-word phrases.</p>
|
||||||
<div style={{ ...styles.topUsersList, maxHeight: 360, overflowY: "auto" }}>
|
<div style={{ ...styles.topUsersList, maxHeight: 360, overflowY: "auto" }}>
|
||||||
{topBigrams.map((item) => (
|
{topBigrams.map((item) => (
|
||||||
<div key={item.ngram} style={styles.topUserItem}>
|
<div key={item.ngram} style={styles.topUserItem}>
|
||||||
@@ -68,7 +73,7 @@ const LinguisticStats = ({ data }: LinguisticStatsProps) => {
|
|||||||
|
|
||||||
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
||||||
<h2 style={styles.sectionTitle}>Top Trigrams</h2>
|
<h2 style={styles.sectionTitle}>Top Trigrams</h2>
|
||||||
<p style={styles.sectionSubtitle}>Most frequent 3-word phrases.</p>
|
<p style={styles.sectionSubtitle}>Most used 3-word phrases.</p>
|
||||||
<div style={{ ...styles.topUsersList, maxHeight: 360, overflowY: "auto" }}>
|
<div style={{ ...styles.topUsersList, maxHeight: 360, overflowY: "auto" }}>
|
||||||
{topTrigrams.map((item) => (
|
{topTrigrams.map((item) => (
|
||||||
<div key={item.ngram} style={styles.topUserItem}>
|
<div key={item.ngram} style={styles.topUserItem}>
|
||||||
|
|||||||
@@ -58,15 +58,13 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
|
|||||||
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
||||||
const selectedUserData: User | null = userData?.users.find((u) => u.author === selectedUser) ?? null;
|
const selectedUserData: User | null = userData?.users.find((u) => u.author === selectedUser) ?? null;
|
||||||
|
|
||||||
console.log(summary)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
|
|
||||||
{/* main grid*/}
|
{/* main grid*/}
|
||||||
<div style={{ ...styles.container, ...styles.grid}}>
|
<div style={{ ...styles.container, ...styles.grid}}>
|
||||||
<Card
|
<Card
|
||||||
label="Total Events"
|
label="Total Activity"
|
||||||
value={summary?.total_events ?? "—"}
|
value={summary?.total_events ?? "—"}
|
||||||
sublabel="Posts + comments"
|
sublabel="Posts + comments"
|
||||||
style={{
|
style={{
|
||||||
@@ -74,15 +72,15 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Unique Users"
|
label="Active People"
|
||||||
value={summary?.unique_users ?? "—"}
|
value={summary?.unique_users ?? "—"}
|
||||||
sublabel="Distinct authors"
|
sublabel="Distinct users"
|
||||||
style={{
|
style={{
|
||||||
gridColumn: "span 4"
|
gridColumn: "span 4"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Posts / Comments"
|
label="Posts vs Comments"
|
||||||
value={
|
value={
|
||||||
summary
|
summary
|
||||||
? `${summary.total_posts} / ${summary.total_comments}`
|
? `${summary.total_posts} / ${summary.total_comments}`
|
||||||
@@ -108,13 +106,13 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
label="Lurker Ratio"
|
label="One-Time Users"
|
||||||
value={
|
value={
|
||||||
typeof summary?.lurker_ratio === "number"
|
typeof summary?.lurker_ratio === "number"
|
||||||
? `${Math.round(summary.lurker_ratio * 100)}%`
|
? `${Math.round(summary.lurker_ratio * 100)}%`
|
||||||
: "—"
|
: "—"
|
||||||
}
|
}
|
||||||
sublabel="Users with only 1 event"
|
sublabel="Users with only one event"
|
||||||
style={{
|
style={{
|
||||||
gridColumn: "span 4"
|
gridColumn: "span 4"
|
||||||
}}
|
}}
|
||||||
@@ -136,12 +134,12 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
|
|||||||
|
|
||||||
{/* events per day */}
|
{/* events per day */}
|
||||||
<div style={{ ...styles.card, gridColumn: "span 5" }}>
|
<div style={{ ...styles.card, gridColumn: "span 5" }}>
|
||||||
<h2 style={styles.sectionTitle}>Events per Day</h2>
|
<h2 style={styles.sectionTitle}>Activity Over Time</h2>
|
||||||
<p style={styles.sectionSubtitle}>Trend of activity over time</p>
|
<p style={styles.sectionSubtitle}>How much posting happened each day.</p>
|
||||||
|
|
||||||
<div style={styles.chartWrapper}>
|
<div style={styles.chartWrapper}>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<LineChart data={timeData?.events_per_day.filter((d) => new Date(d.date) >= new Date('2026-01-10'))}>
|
<LineChart data={timeData?.events_per_day ?? []}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="date" />
|
<XAxis dataKey="date" />
|
||||||
<YAxis />
|
<YAxis />
|
||||||
@@ -154,8 +152,8 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
|
|||||||
|
|
||||||
{/* Word Cloud */}
|
{/* Word Cloud */}
|
||||||
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
<div style={{ ...styles.card, gridColumn: "span 4" }}>
|
||||||
<h2 style={styles.sectionTitle}>Word Cloud</h2>
|
<h2 style={styles.sectionTitle}>Common Words</h2>
|
||||||
<p style={styles.sectionSubtitle}>Most common terms across events</p>
|
<p style={styles.sectionSubtitle}>Frequently used words across the dataset.</p>
|
||||||
|
|
||||||
<div style={styles.chartWrapper}>
|
<div style={styles.chartWrapper}>
|
||||||
<ReactWordcloud
|
<ReactWordcloud
|
||||||
@@ -174,8 +172,8 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
|
|||||||
<div style={{...styles.card, ...styles.scrollArea, gridColumn: "span 3",
|
<div style={{...styles.card, ...styles.scrollArea, gridColumn: "span 3",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2 style={styles.sectionTitle}>Top Users</h2>
|
<h2 style={styles.sectionTitle}>Most Active Users</h2>
|
||||||
<p style={styles.sectionSubtitle}>Most active authors</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) => (
|
{userData?.top_users.slice(0, 100).map((item) => (
|
||||||
@@ -195,8 +193,8 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
|
|||||||
|
|
||||||
{/* Heatmap */}
|
{/* Heatmap */}
|
||||||
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
||||||
<h2 style={styles.sectionTitle}>Heatmap</h2>
|
<h2 style={styles.sectionTitle}>Weekly Activity Pattern</h2>
|
||||||
<p style={styles.sectionSubtitle}>Activity density across time</p>
|
<p style={styles.sectionSubtitle}>When activity tends to happen by weekday and hour.</p>
|
||||||
|
|
||||||
<div style={styles.heatmapWrapper}>
|
<div style={styles.heatmapWrapper}>
|
||||||
<ActivityHeatmap data={timeData?.weekday_hour_heatmap ?? []} />
|
<ActivityHeatmap data={timeData?.weekday_hour_heatmap ?? []} />
|
||||||
@@ -214,4 +212,4 @@ const SummaryStats = ({userData, timeData, contentData, summary}: SummaryStatsPr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SummaryStats;
|
export default SummaryStats;
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function UserModal({ open, onClose, userData, username }: Props) {
|
export default function UserModal({ open, onClose, userData, username }: Props) {
|
||||||
|
const dominantEmotionEntry = Object.entries(userData?.avg_emotions ?? {})
|
||||||
|
.sort((a, b) => b[1] - a[1])[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} style={styles.modalRoot}>
|
<Dialog open={open} onClose={onClose} style={styles.modalRoot}>
|
||||||
<div style={styles.modalBackdrop} />
|
<div style={styles.modalBackdrop} />
|
||||||
@@ -66,6 +69,15 @@ export default function UserModal({ open, onClose, userData, username }: Props)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{dominantEmotionEntry ? (
|
||||||
|
<div style={styles.topUserItem}>
|
||||||
|
<div style={styles.topUserName}>Dominant Avg Emotion</div>
|
||||||
|
<div style={styles.topUserMeta}>
|
||||||
|
{dominantEmotionEntry[0].replace("emotion_", "")} ({dominantEmotionEntry[1].toFixed(3)})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogPanel>
|
</DialogPanel>
|
||||||
|
|||||||
@@ -87,15 +87,15 @@ const UserStats = (props: { data: UserAnalysisResponse }) => {
|
|||||||
style={{ gridColumn: "span 3" }}
|
style={{ gridColumn: "span 3" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Interactions"
|
label="Replies"
|
||||||
value={totalInteractions.toLocaleString()}
|
value={totalInteractions.toLocaleString()}
|
||||||
sublabel="Filtered links (2+ interactions)"
|
sublabel="Links with at least 2 replies"
|
||||||
style={{ gridColumn: "span 3" }}
|
style={{ gridColumn: "span 3" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Average Intensity"
|
label="Replies per Connected User"
|
||||||
value={avgInteractionsPerConnectedUser.toFixed(1)}
|
value={avgInteractionsPerConnectedUser.toFixed(1)}
|
||||||
sublabel="Interactions per connected user"
|
sublabel="Average from visible graph links"
|
||||||
style={{ gridColumn: "span 3" }}
|
style={{ gridColumn: "span 3" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
@@ -106,13 +106,13 @@ const UserStats = (props: { data: UserAnalysisResponse }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
label="Strongest Connection"
|
label="Strongest User Link"
|
||||||
value={strongestLink ? `${strongestLink.source} -> ${strongestLink.target}` : "—"}
|
value={strongestLink ? `${strongestLink.source} -> ${strongestLink.target}` : "—"}
|
||||||
sublabel={strongestLink ? `${strongestLink.value.toLocaleString()} interactions` : "No graph edges after filtering"}
|
sublabel={strongestLink ? `${strongestLink.value.toLocaleString()} replies` : "No graph links after filtering"}
|
||||||
style={{ gridColumn: "span 6" }}
|
style={{ gridColumn: "span 6" }}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
label="Most Reply-Driven User"
|
label="Most Comment-Heavy User"
|
||||||
value={highlyInteractiveUser?.author ?? "—"}
|
value={highlyInteractiveUser?.author ?? "—"}
|
||||||
sublabel={
|
sublabel={
|
||||||
highlyInteractiveUser
|
highlyInteractiveUser
|
||||||
@@ -125,7 +125,7 @@ const UserStats = (props: { data: UserAnalysisResponse }) => {
|
|||||||
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
<div style={{ ...styles.card, gridColumn: "span 12" }}>
|
||||||
<h2 style={styles.sectionTitle}>User Interaction Graph</h2>
|
<h2 style={styles.sectionTitle}>User Interaction Graph</h2>
|
||||||
<p style={styles.sectionSubtitle}>
|
<p style={styles.sectionSubtitle}>
|
||||||
Nodes represent users and links represent conversation interactions.
|
Each node is a user, and each link shows replies between them.
|
||||||
</p>
|
</p>
|
||||||
<div ref={graphContainerRef} style={{ width: "100%", height: graphSize.height }}>
|
<div ref={graphContainerRef} style={{ width: "100%", height: graphSize.height }}>
|
||||||
<ForceGraph3D
|
<ForceGraph3D
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type User = {
|
|||||||
comment: number;
|
comment: number;
|
||||||
comment_post_ratio: number;
|
comment_post_ratio: number;
|
||||||
comment_share: number;
|
comment_share: number;
|
||||||
|
avg_emotions?: Record<string, number>;
|
||||||
vocab?: Vocab | null;
|
vocab?: Vocab | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user