-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
2330 lines (2056 loc) · 113 KB
/
script.js
File metadata and controls
2330 lines (2056 loc) · 113 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* ================================================================
REDDITSCOPE — script.js
Advanced analytics · Canvas charts · Persona detection
Pure vanilla JS — no external libraries
================================================================ */
'use strict';
// ── DOM shortcuts ────────────────────────────────────────────────
const $ = id => document.getElementById(id);
const searchForm = $('searchForm');
const usernameInput = $('usernameInput');
const analyzeBtn = $('analyzeBtn');
const errorMsg = $('errorMsg');
const hero = $('hero');
const loadingSection= $('loadingSection');
const loadingLabel = $('loadingLabel');
const results = $('results');
const themeToggle = $('themeToggle');
const analyzeAgain = $('analyzeAgain');
const copyBtn = $('copyBtn');
const downloadBtn = $('downloadBtn');
const shareBtn = $('shareBtn');
const steps = ['step1','step2','step3','step4'].map(id => $(id));
const CORS_PROXIES = [
url => url,
url => `https://corsproxy.io/?${encodeURIComponent(url)}`,
url => `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`,
];
async function fetchJSON(url) {
let lastErr;
for (const proxy of CORS_PROXIES) {
try {
const res = await fetch(proxy(url), {
headers: { Accept: 'application/json' },
signal: AbortSignal.timeout ? AbortSignal.timeout(9000) : undefined,
});
if (res.status === 429) throw new Error('Rate limited — please wait a moment.');
if (!res.ok && res.status !== 404) { lastErr = new Error(`HTTP ${res.status}`); continue; }
return await res.json();
} catch(e) {
if (e.message.includes('Rate')) throw e;
lastErr = e;
}
}
throw new Error('Could not reach Reddit API. Try hosting via a local server instead of file://');
}
// ── Theme ────────────────────────────────────────────────────────
themeToggle.addEventListener('click', () => {
const t = document.documentElement.getAttribute('data-theme');
document.documentElement.setAttribute('data-theme', t === 'dark' ? 'light' : 'dark');
});
// ── Star canvas background ───────────────────────────────────────
(function initStars() {
const canvas = $('bgStars');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let stars = [];
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
stars = Array.from({ length: 120 }, () => ({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: Math.random() * 1.2 + 0.2,
a: Math.random(),
s: Math.random() * 0.003 + 0.001,
}));
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
stars.forEach(s => {
s.a += s.s; if (s.a > 1) s.s = -s.s;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${Math.abs(s.a) * 0.6})`;
ctx.fill();
});
requestAnimationFrame(draw);
}
resize(); draw();
window.addEventListener('resize', resize);
})();
analyzeAgain.addEventListener('click', () => {
results.hidden = true;
hero.hidden = false;
hero.style.display = '';
usernameInput.value = '';
clearError();
const ddPanel = document.getElementById('digDeepPanel');
const ddBtn = document.getElementById('digDeepBtn');
if (ddPanel) ddPanel.hidden = true;
if (ddBtn) { ddBtn.disabled = false; ddBtn.innerHTML = '<span class="dig-deep-icon">🔍</span><span>Let\u2019s Dig Deep</span>'; }
window._currentPosts = [];
window._currentComments = [];
usernameInput.focus();
window.scrollTo({ top: 0, behavior: 'smooth' });
});
searchForm.addEventListener('submit', async e => {
e.preventDefault();
const username = usernameInput.value.trim().replace(/^u\//i, '');
if (!username) return showError('Please enter a Reddit username.');
await analyzeUser(username);
});
// ── Main orchestrator ────────────────────────────────────────────
async function analyzeUser(username) {
clearError();
setLoading(true);
resetSteps();
try {
setStep(0, 'active');
const profile = await fetchProfile(username);
setStep(0, 'done'); setStep(1, 'active');
// Check if profile is hidden (posts hidden from /submitted endpoint)
updateLoadingLabel('Checking profile visibility…');
const profileIsHidden = await checkIfHidden(username);
profile._isHidden = profileIsHidden;
updateLoadingLabel('Fetching posts…');
let posts = await fetchListing(username, 'submitted');
// If hidden or fewer than 5 posts came back, try search-index fallback
if (profileIsHidden || posts.length < 5) {
updateLoadingLabel(profileIsHidden
? 'Hidden profile — searching index…'
: 'Low results — searching index…');
const searchPosts = await fetchListingBySearch(username);
if (searchPosts.length > posts.length) {
posts = searchPosts;
profile._usedSearchFallback = true;
}
}
setStep(1, 'done'); setStep(2, 'active');
updateLoadingLabel('Fetching comments…');
const comments = await fetchListing(username, 'comments');
setStep(2, 'done'); setStep(3, 'active');
updateLoadingLabel('Crunching numbers…');
const analysis = analyzeData(profile, posts, comments);
setStep(3, 'done');
await sleep(400);
renderResults(profile, posts, comments, analysis);
setLoading(false);
hero.style.display = 'none'; // keep hero hidden while showing results
results.hidden = false;
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (err) {
setLoading(false);
hero.style.display = ''; // restore hero on error
hero.hidden = false;
showError(err.message || 'Something went wrong. Try again.');
console.error('[RedditScope]', err);
}
}
// ── API fetchers ─────────────────────────────────────────────────
async function fetchProfile(username) {
updateLoadingLabel('Fetching profile…');
const url = `https://www.reddit.com/user/${encodeURIComponent(username)}/about.json`;
const res = await fetchJSON(url);
if (res.error === 404 || res.message === 'Not Found') throw new Error(`u/${username} not found.`);
if (res.data?.is_suspended) throw new Error(`u/${username} is suspended.`);
if (!res.data || res.kind !== 't2') throw new Error('Invalid API response.');
return res.data;
}
async function fetchListing(username, type) {
const MAX_PAGES = 10;
const base = `https://www.reddit.com/user/${encodeURIComponent(username)}/${type}.json?limit=100`;
let items = [], after = null, page = 0;
while (page < MAX_PAGES) {
const url = after ? `${base}&after=${after}` : base;
let res;
try { res = await fetchJSON(url); } catch(e) { break; }
if (!res?.data?.children?.length) break;
const batch = res.data.children.map(c => c.data).filter(Boolean);
items = items.concat(batch);
page++;
after = res.data.after;
if (!after) break;
await sleep(120);
}
return items;
}
// ── Hidden profile detection ──────────────────────────────────────
// When a user hides their profile, /submitted returns empty.
// But Reddit's search index still has their posts via author:"username".
async function checkIfHidden(username) {
try {
const url = `https://www.reddit.com/user/${encodeURIComponent(username)}/submitted.json?limit=10`;
const res = await fetchJSON(url);
if (!res?.data?.children) return true; // can't read → treat as hidden
const children = res.data.children || [];
if (children.length === 0) return true;
// If all returned posts are in their own user-profile subreddit, they're hidden
const nonProfile = children.filter(c => c.data && c.data.subreddit_type !== 'user');
return nonProfile.length === 0;
} catch (_) { return false; }
}
// ── Search-index post fetch (works on hidden profiles) ─────────────
// Uses /search.json?q=author:"username" — Reddit's search index
// indexes posts even when user profile is set to hidden.
async function fetchListingBySearch(username) {
const q = `author:"${username}"`;
const base = `https://www.reddit.com/search.json?q=${encodeURIComponent(q)}&sort=relevance&limit=100&type=link`;
let items = [], after = null, page = 0, seenIds = new Set();
const MAX = 10;
while (page < MAX) {
const url = after ? `${base}&after=${after}` : base;
let res;
try { res = await fetchJSON(url); } catch(e) { break; }
if (!res?.data?.children?.length) break;
const batch = res.data.children
.filter(c => c.data && !seenIds.has(c.data.id))
.map(c => { seenIds.add(c.data.id); return c.data; });
items = items.concat(batch);
page++;
after = res.data.after;
if (!after) break;
await sleep(150);
}
return items;
}
// ── Data Analysis Engine ─────────────────────────────────────────
function analyzeData(profile, posts, comments) {
const allItems = [...posts, ...comments];
const now = Date.now() / 1000;
// ── Subreddit frequency
const subCount = {};
allItems.forEach(item => {
if (item.subreddit) subCount[item.subreddit] = (subCount[item.subreddit] || 0) + 1;
});
const topSubs = Object.entries(subCount).sort((a,b) => b[1]-a[1]).slice(0, 8)
.map(([name, count]) => ({ name, count, pct: Math.round((count / allItems.length) * 100) }));
// ── Hour-of-day distribution (0-23)
const hourDist = new Array(24).fill(0);
allItems.forEach(item => {
if (item.created_utc) hourDist[new Date(item.created_utc * 1000).getHours()]++;
});
const peakHour = hourDist.indexOf(Math.max(...hourDist));
// ── Day-of-week distribution (0=Sun, 6=Sat)
const dowDist = new Array(7).fill(0);
allItems.forEach(item => {
if (item.created_utc) dowDist[new Date(item.created_utc * 1000).getDay()]++;
});
const peakDow = dowDist.indexOf(Math.max(...dowDist));
// ── Content type breakdown
const typeMap = { image: 0, link: 0, text: 0, video: 0, gallery: 0 };
posts.forEach(p => {
if (p.is_video) typeMap.video++;
else if (p.is_gallery) typeMap.gallery++;
else if (p.is_self) typeMap.text++;
else if (p.url && /\.(jpg|jpeg|png|gif|webp)/i.test(p.url)) typeMap.image++;
else typeMap.link++;
});
// ── Best post & comment
const topPost = posts.length ? posts.reduce((a,b) => b.score > a.score ? b : a) : null;
const topComment = comments.length ? comments.reduce((a,b) => b.score > a.score ? b : a) : null;
// ── Post scores
const avgPostScore = posts.length ? Math.round(posts.reduce((s,p) => s+(p.score||0),0)/posts.length) : 0;
const avgComments = posts.length ? (posts.reduce((s,p) => s+(p.num_comments||0),0)/posts.length).toFixed(1) : 0;
// ── Posting frequency
const ageMonths = (now - profile.created_utc) / (60*60*24*30);
const postsPerMonth = ageMonths > 0 ? (posts.length / ageMonths).toFixed(1) : posts.length;
// ── Comment/post ratio
const ratio = posts.length > 0 ? (comments.length / posts.length).toFixed(1) : comments.length;
// ── Controversiality score (0-100)
const controversialPosts = posts.filter(p => p.upvote_ratio && p.upvote_ratio < 0.6).length;
const controversiality = posts.length ? Math.round((controversialPosts / posts.length) * 100) : 0;
// ── Sentiment analysis (simple lexicon)
const sentimentScore = calcSentiment(posts, comments);
// ── Word frequency
const wordFreq = buildWordFrequency(posts, comments);
// ── Most active day ever
const dayMap = {};
allItems.forEach(item => {
if (!item.created_utc) return;
const d = new Date(item.created_utc * 1000);
const key = d.toDateString();
dayMap[key] = (dayMap[key] || 0) + 1;
});
const mostActiveDay = Object.entries(dayMap).sort((a,b) => b[1]-a[1])[0] || null;
// ── Award count
const awards = allItems.reduce((sum, item) => sum + (item.total_awards_received || 0), 0);
// ── Persona detection
const persona = detectPersona(posts, comments, topSubs, ratio, avgPostScore, sentimentScore, hourDist);
return {
topSubs, hourDist, dowDist, typeMap,
topPost, topComment, avgPostScore, avgComments,
postsPerMonth, ratio, controversiality,
sentimentScore, wordFreq, mostActiveDay,
peakHour, peakDow, awards, persona,
totalItems: (posts ? posts.length : 0) + (comments ? comments.length : 0),
};
}
// ── Sentiment ────────────────────────────────────────────────────
function calcSentiment(posts, comments) {
const POS = new Set(['good','great','best','love','awesome','amazing','excellent','wonderful','fantastic',
'happy','glad','thanks','thank','helpful','nice','perfect','brilliant','beautiful','enjoy','enjoyed',
'useful','interesting','incredible','impressive','outstanding','positive','agree','correct','right']);
const NEG = new Set(['bad','worst','hate','awful','terrible','horrible','disgusting','wrong','broken',
'stupid','idiot','dumb','annoying','fail','failed','disappointed','useless','pathetic','garbage',
'trash','scam','lie','lying','fake','false','misleading','disagree','incorrect','evil']);
let pos = 0, neg = 0;
const addText = txt => {
if (!txt) return;
txt.toLowerCase().split(/\W+/).forEach(w => {
if (POS.has(w)) pos++;
if (NEG.has(w)) neg++;
});
};
posts.forEach(p => addText(p.title));
comments.forEach(c => addText(c.body));
const total = pos + neg;
if (!total) return 50;
return Math.round((pos / total) * 100);
}
// ── Word frequency ────────────────────────────────────────────────
function buildWordFrequency(posts, comments) {
const STOP = new Set([
'the','a','an','and','or','but','in','on','at','to','for','of','with','is','are','was','were',
'be','been','have','has','had','do','does','did','will','would','could','should','may','might',
'this','that','these','those','i','my','me','we','our','you','your','he','his','she','her',
'they','their','it','its','not','so','as','if','by','from','up','out','about','just','no',
'more','when','what','all','one','can','get','like','than','then','there','also','into','after',
'before','how','which','who','re','https','www','http','amp','gt','lt','edit','deleted','removed',
'really','very','much','still','even','some','only','any','other','been','same','than','too',
'most','over','such','back','well','know','think','want','need','dont','cant','wont','its',
'here','just','now','people','time','year','make','made','use','used','going','come','see',
]);
const freq = {};
const add = txt => {
if (!txt) return;
txt.toLowerCase().replace(/[^a-z0-9\s]/g,' ').split(/\s+/)
.filter(w => w.length > 3 && !STOP.has(w) && isNaN(w))
.forEach(w => { freq[w] = (freq[w]||0)+1; });
};
posts.forEach(p => add(p.title));
comments.forEach(c => add(c.body));
return Object.entries(freq).sort((a,b) => b[1]-a[1]).slice(0,40);
}
// ── Persona detection ─────────────────────────────────────────────
function detectPersona(posts, comments, topSubs, ratio, avgScore, sentiment, hourDist) {
const nightPosts = hourDist.slice(22).concat(hourDist.slice(0,5)).reduce((a,b)=>a+b,0);
const morningPosts = hourDist.slice(6,10).reduce((a,b)=>a+b,0);
const totalAct = hourDist.reduce((a,b)=>a+b,0) || 1;
const isNightOwl = nightPosts / totalAct > 0.3;
const isEarlyBird = morningPosts / totalAct > 0.35;
const freq = comments.length + posts.length;
// Edge cases
if (posts.length === 0 && comments.length > 10)
return { icon: '👀', label: 'Silent Observer', desc: 'Lurks and comments, never starts the conversation' };
if (posts.length === 0 && comments.length <= 10)
return { icon: '🕵️', label: 'Ghost', desc: 'Barely leaves a trace on Reddit' };
if (comments.length === 0 && posts.length > 0)
return { icon: '📢', label: 'Broadcaster', desc: 'Posts content but rarely engages in replies' };
// Score-based archetypes
if (avgScore > 5000) return { icon: '🌟', label: 'Reddit Legend', desc: 'Posts consistently dominate the frontpage' };
if (avgScore > 1000) return { icon: '🏆', label: 'Viral Creator', desc: 'Content regularly goes viral across Reddit' };
if (avgScore > 500) return { icon: '🔥', label: 'Trending Machine', desc: 'Consistently reaches hot with quality content' };
// Behavior-based
if (Number(ratio) > 25) return { icon: '💬', label: 'Comment Dynamo', desc: 'Lives in the comments, almost never posts' };
if (Number(ratio) > 10) return { icon: '🗣️', label: 'Conversationalist', desc: 'Loves discussions far more than posting' };
if (Number(ratio) < 0.5 && posts.length > 10)
return { icon: '📸', label: 'Pure Creator', desc: 'Posts prolifically and rarely replies' };
// Sentiment-based
if (sentiment > 80) return { icon: '☀️', label: 'Positivity Beacon', desc: 'Relentlessly upbeat, a ray of sunshine on Reddit' };
if (sentiment > 70) return { icon: '😊', label: 'Positive Force', desc: 'Spreads warmth and positivity across communities' };
if (sentiment < 25) return { icon: '⚔️', label: 'Edgelord', desc: 'Deeply contrarian, thrives in debate and conflict' };
if (sentiment < 35) return { icon: '🌩️', label: 'Contrarian', desc: 'Challenges prevailing views and loves a good argument' };
// Time-based
if (isNightOwl && Number(ratio) > 5)
return { icon: '🦉', label: 'Midnight Debater', desc: 'Comes alive in comment sections after dark' };
if (isNightOwl) return { icon: '🦉', label: 'Night Owl', desc: 'Most active during the late-night hours' };
if (isEarlyBird) return { icon: '🌅', label: 'Early Bird', desc: 'Greets the Reddit day before most others wake up' };
// Niche
if (topSubs.length === 1)
return { icon: '🎯', label: 'Niche Specialist', desc: `Laser-focused on r/${topSubs[0]?.name}` };
if (topSubs.length === 2)
return { icon: '🎲', label: 'Dual Citizen', desc: `Splits time between r/${topSubs[0]?.name} and r/${topSubs[1]?.name}` };
// Frequency
if (freq > 300 && avgScore > 200)
return { icon: '⭐', label: 'Power User', desc: 'Prolific, high-quality contributor' };
if (freq > 200) return { icon: '⚡', label: 'Hyperactive', desc: 'Posts and comments at a relentless pace' };
// Community breadth
if (topSubs.length >= 7)
return { icon: '🌐', label: 'Community Hopper', desc: 'Deeply involved across a wide range of subreddits' };
// Default
return { icon: '🧭', label: 'Explorer', desc: 'Curious generalist ranging across many communities' };
}
// ── Post-per-month calc ───────────────────────────────────────────
function calcPostsPerMonth(posts, createdUtc) {
if (!posts.length) return 0;
const ageM = (Date.now()/1000 - createdUtc) / (60*60*24*30);
return ageM > 0 ? (posts.length / ageM).toFixed(1) : posts.length;
}
// ================================================================
// RENDERING
// ================================================================
function renderResults(profile, posts, comments, analysis) {
window._currentPosts = posts;
window._currentComments = comments;
window._currentProfile = profile;
renderProfileHero(profile, posts, comments, analysis);
renderTicker(profile, posts, comments, analysis);
renderMetrics(profile, posts, comments, analysis);
renderHourChart(analysis.hourDist, analysis.peakHour);
renderTypeDonut(analysis.typeMap);
renderHeatmap(analysis.dowDist);
renderSubreddits(analysis.topSubs);
renderBestContent(analysis);
renderWordCloud(analysis.wordFreq);
renderPersonalityType(profile, posts, comments, analysis);
renderReportCard(profile, posts, comments, analysis);
renderRoast(profile, posts, comments, analysis);
renderSummary(profile, posts, comments, analysis);
setupActions(profile, analysis);
animateIn();
}
// ── Profile Hero ─────────────────────────────────────────────────
function renderProfileHero(profile, posts, comments, analysis) {
// Avatar
const av = $('profileAvatar');
const img = profile.icon_img || profile.snoovatar_img || '';
av.src = img ? img.split('?')[0] : defaultAvatar();
av.onerror = () => { av.src = defaultAvatar(); };
// Status dot color based on sentiment
const statusEl = $('avatarStatus');
statusEl.style.background = analysis.sentimentScore > 60 ? '#06d6a0' : analysis.sentimentScore > 40 ? '#f0b429' : '#ff6b6b';
// Name
$('profileUsername').textContent = `u/${profile.name}`;
// Badges
const badges = $('profileBadges');
badges.innerHTML = '';
const age = accountAge(profile.created_utc);
badges.innerHTML += `<span class="badge badge-age">🎂 ${age} old</span>`;
if (profile._isHidden) badges.innerHTML += `<span class="badge badge-hidden">👻 Hidden Profile</span>`;
if (profile.is_employee) badges.innerHTML += `<span class="badge badge-employee">🏢 Reddit Employee</span>`;
if (profile.is_gold) badges.innerHTML += `<span class="badge badge-premium">★ Premium</span>`;
if (profile.has_verified_email) badges.innerHTML += `<span class="badge badge-verified">✓ Verified</span>`;
// Since
const cakeDay = new Date(profile.created_utc * 1000).toLocaleDateString('en-US', { year:'numeric', month:'long', day:'numeric' });
$('profileSince').textContent = `Member since ${cakeDay}`;
// Show/update the hidden profile notice
let hiddenNotice = document.getElementById('hiddenProfileNotice');
if (profile._isHidden || profile._usedSearchFallback) {
if (!hiddenNotice) {
hiddenNotice = document.createElement('div');
hiddenNotice.id = 'hiddenProfileNotice';
hiddenNotice.className = 'hidden-profile-notice';
const resultsEl = document.getElementById('results');
const profileHeroEl = document.getElementById('profileHero');
resultsEl.insertBefore(hiddenNotice, profileHeroEl.nextSibling);
}
const mode = profile._usedSearchFallback
? 'Posts were fetched via Reddit\'s search index'
: 'Profile is hidden';
const explanation = profile._isHidden
? 'This user has hidden their profile their posts are invisible on their profile page, but were recovered from Reddit\'s search index.'
: 'Fewer posts were found via the profile endpoint, so the search index was used to recover more.';
hiddenNotice.innerHTML = `
<div class="hn-icon">👻</div>
<div class="hn-body">
<div class="hn-title">${mode}</div>
<div class="hn-desc">${explanation} Comments from hidden profiles cannot be recovered.</div>
</div>
`;
hiddenNotice.style.display = 'flex';
} else if (hiddenNotice) {
hiddenNotice.style.display = 'none';
}
// Karma donut
const postK = profile.link_karma || 0;
const commentK = profile.comment_karma || 0;
const total = postK + commentK;
drawDonut($('karmaDonut'), [
{ val: postK, color: '#ff4500' },
{ val: commentK, color: '#9b5de5' },
], 6);
const kcv = $('totalKarmaVal');
kcv.innerHTML = `<div class="kv-num">${formatNumber(total)}</div><div class="kv-lbl">KARMA</div>`;
$('karmaLegend').innerHTML = `
<div class="kleg-item"><div class="kleg-dot" style="background:#ff4500"></div><span class="kleg-text">Post ${formatNumber(postK)}</span></div>
<div class="kleg-item"><div class="kleg-dot" style="background:#9b5de5"></div><span class="kleg-text">Comment ${formatNumber(commentK)}</span></div>
`;
// Persona
$('personaIcon').textContent = analysis.persona.icon;
$('personaLabel').textContent = analysis.persona.label;
// BG gradient
$('profileHeroBg').style.background = `linear-gradient(135deg, rgba(255,69,0,0.1) 0%, rgba(155,93,229,0.06) 100%)`;
}
// ── Stats Ticker ──────────────────────────────────────────────────
function renderTicker(profile, posts, comments, analysis) {
const total = (profile.link_karma||0) + (profile.comment_karma||0);
const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const items = [
['Total Karma', formatNumber(total), false],
['Posts', formatNumber(posts.length), false],
['Comments', formatNumber(comments.length), false],
['Avg Score', formatNumber(analysis.avgPostScore), false],
['Persona', analysis.persona.label, true],
['Sentiment', `${analysis.sentimentScore}% positive`, false],
['Peak Day', analysis.peakDow !== undefined ? DAYS[analysis.peakDow] : '—', false],
['Posts/Month', analysis.postsPerMonth, false],
['Top Sub', `r/${analysis.topSubs[0]?.name||'—'}`, true],
['Comment Ratio',`${analysis.ratio}:1`, false],
['Awards', formatNumber(analysis.awards), false],
['Account Age', accountAge(profile.created_utc), false],
];
const track = $('tickerTrack');
const buildItems = () => items.map(([lbl, val, accent]) =>
`<div class="ticker-item">
<span class="ticker-item-label">${lbl}</span>
<span class="${accent ? 'ticker-item-accent' : 'ticker-item-value'}">${val}</span>
</div>`
).join('');
// Duplicate for seamless loop
track.innerHTML = buildItems() + buildItems();
}
// ── Metrics Grid ──────────────────────────────────────────────────
function renderMetrics(profile, posts, comments, analysis) {
const scoreLabel = analysis.avgPostScore > 500 ? 'Exceptional' : analysis.avgPostScore > 100 ? 'Above average' : analysis.avgPostScore > 20 ? 'Moderate' : 'Modest';
const scoreClass = analysis.avgPostScore > 100 ? 'good' : analysis.avgPostScore > 20 ? 'warn' : '';
const sentiment = analysis.sentimentScore;
const sentLabel = sentiment > 70 ? 'Very positive' : sentiment > 55 ? 'Positive' : sentiment > 45 ? 'Neutral' : sentiment > 30 ? 'Critical' : 'Very negative';
const sentClass = sentiment > 55 ? 'good' : sentiment > 40 ? 'warn' : 'bad';
const contClass = analysis.controversiality > 30 ? 'bad' : analysis.controversiality > 15 ? 'warn' : 'good';
function setM(id, val, sub) {
const el = $(id);
if (!el) return;
el.querySelector('.metric-val').textContent = val;
const s = el.querySelector('.metric-sub');
if (s) s.innerHTML = sub;
animateCounter(el.querySelector('.metric-val'));
}
setM('mPosts', formatNumber(posts.length), `<span>${posts.length >= 1000 ? '1,000 max fetched' : 'All fetched'}</span>`);
setM('mComments', formatNumber(comments.length), `<span>${comments.length >= 1000 ? '1,000 max fetched' : 'All fetched'}</span>`);
setM('mAvgScore', formatNumber(analysis.avgPostScore), `<span class="${scoreClass}">${scoreLabel}</span>`);
setM('mFreq', analysis.postsPerMonth, `<span>Per calendar month</span>`);
setM('mEngagement', analysis.avgComments, `<span>Avg discussion per post</span>`);
setM('mRatio', `${analysis.ratio}:1`, `<span>Comments vs posts</span>`);
setM('mSentiment', `${sentiment}%`, `<span class="${sentClass}">${sentLabel}</span>`);
setM('mControversy', `${analysis.controversiality}%`, `<span class="${contClass}">${analysis.controversiality > 20 ? 'Polarizing' : 'Uncontroversial'}</span>`);
}
// ── Hour Chart (bar) ──────────────────────────────────────────────
function renderHourChart(hourDist, peakHour) {
const canvas = $('hourChart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const maxVal = Math.max(...hourDist, 1);
const barW = W / 24;
const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
ctx.clearRect(0, 0, W, H);
hourDist.forEach((val, hour) => {
const bh = Math.max((val / maxVal) * (H - 10), 2);
const x = hour * barW;
const isPeak = hour === peakHour;
const grad = ctx.createLinearGradient(0, H - bh, 0, H);
grad.addColorStop(0, isPeak ? 'rgba(255,69,0,0.95)' : 'rgba(255,69,0,0.45)');
grad.addColorStop(1, isPeak ? 'rgba(255,140,90,0.4)' : 'rgba(255,69,0,0.1)');
ctx.fillStyle = grad;
const radius = Math.min(4, barW * 0.3);
const bx = x + 1, by = H - bh, bw = barW - 2;
ctx.beginPath();
ctx.moveTo(bx + radius, by);
ctx.lineTo(bx + bw - radius, by);
ctx.quadraticCurveTo(bx + bw, by, bx + bw, by + radius);
ctx.lineTo(bx + bw, H);
ctx.lineTo(bx, H);
ctx.lineTo(bx, by + radius);
ctx.quadraticCurveTo(bx, by, bx + radius, by);
ctx.closePath();
ctx.fill();
if (isPeak) {
ctx.fillStyle = 'rgba(255,69,0,0.15)';
ctx.fillRect(x, 0, barW, H);
}
});
// Peak label
const peakLabel = `Peak: ${peakHour === 0 ? '12am' : peakHour < 12 ? `${peakHour}am` : peakHour === 12 ? '12pm' : `${peakHour-12}pm`}`;
$('peakHourLabel').textContent = peakLabel;
}
// ── Type Donut ────────────────────────────────────────────────────
function renderTypeDonut(typeMap) {
const canvas = $('typeDonut');
if (!canvas) return;
const entries = Object.entries(typeMap).filter(([,v]) => v > 0);
if (!entries.length) return;
const COLORS = { text: '#ff4500', link: '#9b5de5', image: '#4cc9f0', video: '#f72585', gallery: '#f0b429' };
const LABELS = { text: 'Text', link: 'Link', image: 'Image', video: 'Video', gallery: 'Gallery' };
const total = entries.reduce((s,[,v]) => s+v, 0);
const segments = entries.map(([k,v]) => ({ key: k, val: v, color: COLORS[k], pct: Math.round((v/total)*100) }));
drawDonut(canvas, segments.map(s => ({ val: s.val, color: s.color })), 10);
// Center
const topType = segments.sort((a,b) => b.val-a.val)[0];
$('typeDonutCenter').innerHTML = `<div class="td-val">${topType.pct}%</div><div class="td-lbl">${LABELS[topType.key]}</div>`;
// Legend
$('typeLegend').innerHTML = segments.sort((a,b) => b.val-a.val).map(s => `
<div class="tleg">
<div class="tleg-left"><div class="tleg-dot" style="background:${s.color}"></div><span class="tleg-name">${LABELS[s.key]}</span></div>
<span class="tleg-pct">${s.pct}%</span>
</div>
`).join('');
}
// ── Donut draw helper ─────────────────────────────────────────────
function drawDonut(canvas, segments, gap = 4) {
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const cx = W / 2, cy = H / 2;
const outerR = Math.min(cx, cy) - 4;
const innerR = outerR * 0.62;
const total = segments.reduce((s, seg) => s + seg.val, 0) || 1;
ctx.clearRect(0, 0, W, H);
let startAngle = -Math.PI / 2;
const gapAngle = (gap / (2 * Math.PI * outerR)) * (2 * Math.PI);
segments.forEach(seg => {
const sliceAngle = (seg.val / total) * (Math.PI * 2) - gapAngle;
if (sliceAngle <= 0) return;
ctx.beginPath();
ctx.arc(cx, cy, outerR, startAngle, startAngle + sliceAngle);
ctx.arc(cx, cy, innerR, startAngle + sliceAngle, startAngle, true);
ctx.closePath();
ctx.fillStyle = seg.color;
ctx.fill();
startAngle += sliceAngle + gapAngle;
});
}
// ── Weekly Heatmap ────────────────────────────────────────────────
function renderHeatmap(dowDist) {
const DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
const maxVal = Math.max(...dowDist, 1);
const wrap = $('heatmapWrap');
if (!wrap) return;
// Build a richer 7-hour-group heatmap using all data
// We have dowDist (7 values) — show them as colored blocks per day
wrap.innerHTML = '';
// Single row with 7 blocks
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:8px;';
dowDist.forEach((count, i) => {
const intensity = maxVal > 0 ? count / maxVal : 0;
const el = document.createElement('div');
el.style.cssText = `flex:1;display:flex;flex-direction:column;gap:6px;align-items:center;`;
const bar = document.createElement('div');
bar.style.cssText = `width:100%;border-radius:8px;background:rgba(255,69,0,${0.08 + intensity * 0.85});
border:1px solid rgba(255,69,0,${0.15 + intensity * 0.5});
height:60px;position:relative;transition:all 0.3s;cursor:default;`;
bar.setAttribute('data-tip', `${DAYS[i]}: ${count} actions`);
bar.addEventListener('mouseenter', () => { bar.style.transform = 'scaleY(1.05)'; });
bar.addEventListener('mouseleave', () => { bar.style.transform = ''; });
const lbl = document.createElement('div');
lbl.style.cssText = `font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text-muted);`;
lbl.textContent = DAYS[i];
const cnt = document.createElement('div');
cnt.style.cssText = `font-size:11px;color:${intensity > 0.5 ? 'var(--accent)' : 'var(--text-muted)'};font-weight:600;font-family:'JetBrains Mono',monospace;`;
cnt.textContent = count;
el.append(bar, lbl, cnt);
row.appendChild(el);
});
wrap.appendChild(row);
}
// ── Subreddits ────────────────────────────────────────────────────
function renderSubreddits(topSubs) {
const list = $('subredditList');
list.innerHTML = '';
if (!topSubs.length) { list.innerHTML = '<p style="color:var(--text-muted)">No activity found.</p>'; return; }
const maxCount = topSubs[0].count;
topSubs.forEach((sub, i) => {
const relPct = maxCount > 0 ? Math.round((sub.count / maxCount) * 100) : 0;
const el = document.createElement('div');
el.className = 'sub-item';
el.innerHTML = `
<div class="sub-rank">${i+1}</div>
<div class="sub-body">
<div class="sub-name"><a href="https://reddit.com/r/${sub.name}" target="_blank" rel="noopener">r/${sub.name}</a></div>
<div class="sub-bar-row">
<div class="sub-bar-bg"><div class="sub-bar-fill" style="width:0%" data-target="${relPct}%"></div></div>
<div class="sub-bar-lbl" data-count="${sub.count}">${sub.pct}% of activity</div>
</div>
</div>
<div class="sub-count-badge">
<span class="scb-n">${sub.count}</span>
<span class="scb-l">interactions</span>
</div>`;
list.appendChild(el);
requestAnimationFrame(() => setTimeout(() => {
const fill = el.querySelector('.sub-bar-fill');
if (fill) fill.style.width = fill.dataset.target;
}, 100 + i * 80));
});
}
// ── Best Content ──────────────────────────────────────────────────
function renderBestContent(analysis) {
// Top post
const tp = $('topPost');
if (analysis.topPost) {
const p = analysis.topPost;
tp.innerHTML = `
<div class="best-item-sub">r/${escapeHTML(p.subreddit)}</div>
<div class="best-item-title">${escapeHTML(p.title)}</div>
<div class="best-item-score">▲ ${formatNumber(p.score)} upvotes · ${formatNumber(p.num_comments||0)} comments</div>`;
} else {
tp.innerHTML = '<p style="color:var(--text-muted)">No posts found.</p>';
}
// Top comment
const tc = $('topComment');
if (analysis.topComment) {
const c = analysis.topComment;
const body = (c.body||'').substring(0, 240) + ((c.body||'').length > 240 ? '…' : '');
tc.innerHTML = `
<div class="best-item-sub">r/${escapeHTML(c.subreddit)}</div>
<div class="best-item-body">${escapeHTML(body)}</div>
<div class="best-item-score">▲ ${formatNumber(c.score)} upvotes</div>`;
} else {
tc.innerHTML = '<p style="color:var(--text-muted)">No comments found.</p>';
}
// Most active day
const mad = $('mostActiveDay');
if (analysis.mostActiveDay) {
const [dateStr, count] = analysis.mostActiveDay;
const d = new Date(dateStr);
const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
mad.innerHTML = `
<div class="best-day-name">${DAYS[d.getDay()]}</div>
<div class="best-day-sub">${d.toLocaleDateString('en-US', { year:'numeric', month:'long', day:'numeric' })}</div>
<div class="best-day-stat">${count} posts & comments on this day</div>`;
} else {
mad.innerHTML = '<p style="color:var(--text-muted)">Not enough data.</p>';
}
}
// ── Word Cloud ────────────────────────────────────────────────────
function renderWordCloud(wordFreq) {
const wrap = $('wordCloud');
wrap.innerHTML = '';
if (!wordFreq.length) { wrap.innerHTML = '<p style="color:var(--text-muted)">Not enough text.</p>'; return; }
const maxF = wordFreq[0][1];
wordFreq.forEach(([word, freq], i) => {
const size = 11 + Math.round((freq / maxF) * 18);
const opacity = 0.5 + (freq / maxF) * 0.5;
const el = document.createElement('span');
el.className = 'word-pill';
el.textContent = word;
el.style.fontSize = `${size}px`;
el.style.opacity = opacity;
el.style.animationDelay = `${i * 0.02}s`;
el.title = `"${word}" — used ${freq} time${freq !== 1 ? 's' : ''}`;
wrap.appendChild(el);
});
}
// ── AI Summary ────────────────────────────────────────────────────
function renderSummary(profile, posts, comments, analysis) {
const name = `u/${profile.name}`;
$('summaryUsername').textContent = name;
// Influence score (0–100) from karma, engagement, and awards
const total = (profile.link_karma||0) + (profile.comment_karma||0);
const karmaScore = Math.min(40, Math.round(Math.log10(Math.max(total,1)) / Math.log10(1e7) * 40));
const engageScore = Math.min(35, Math.round(Math.min(analysis.avgPostScore, 2000) / 2000 * 35));
const awardScore = Math.min(15, analysis.awards);
const frequScore = Math.min(10, Math.round(Math.min(parseFloat(analysis.postsPerMonth), 30) / 30 * 10));
const influenceScore = karmaScore + engageScore + awardScore + frequScore;
const scoreEl = $('summaryEngagementScore');
if (scoreEl) {
scoreEl.textContent = '0';
animateCounter(scoreEl, influenceScore, '', 1200);
}
// Overview paragraph
const overview = buildOverview(profile, posts, comments, analysis);
$('summaryText').textContent = overview;
window._currentSummary = overview;
window._currentProfile = profile;
window._currentAnalysis = analysis;
// Personality bars
renderPersonalityBars(profile, posts, comments, analysis);
// Activity profile data
renderActivityProfile(profile, posts, comments, analysis);
// Content style data
renderContentStyle(profile, posts, comments, analysis);
// Behavioral insight
const insight = buildBehavioralInsight(profile, posts, comments, analysis);
const insightEl = $('behavioralInsight');
if (insightEl) insightEl.textContent = insight;
// Traits
const traits = buildTraits(profile, posts, comments, analysis);
$('summaryTraits').innerHTML = traits.map(t => `<span class="trait-chip">${t}</span>`).join('');
// Animate personality bars after render
setTimeout(() => {
document.querySelectorAll('.pbar-fill').forEach(bar => {
bar.style.width = bar.dataset.target || '0%';
});
}, 300);
}
function buildOverview(profile, posts, comments, analysis) {
const name = `u/${profile.name}`;
const age = accountAge(profile.created_utc);
const total = (profile.link_karma||0) + (profile.comment_karma||0);
const top3 = analysis.topSubs.slice(0,3).map(s => `r/${s.name}`);
const topW = analysis.wordFreq.slice(0,4).map(([w]) => w);
const HOURS = ['midnight','the early hours','the morning','the morning','the morning','the early hours',
'the morning','the morning','the morning','the morning','the morning','midday','midday',
'the afternoon','the afternoon','the afternoon','the afternoon','the evening','the evening',
'the evening','late night','late night','late night','the early hours'];
const DAYS_FULL = ['Sundays','Mondays','Tuesdays','Wednesdays','Thursdays','Fridays','Saturdays'];
const engagement = analysis.avgPostScore > 1000 ? 'viral-level' : analysis.avgPostScore > 500 ? 'exceptional' :
analysis.avgPostScore > 100 ? 'strong' : analysis.avgPostScore > 20 ? 'moderate' : 'modest';
const sentDesc = analysis.sentimentScore > 75 ? 'unmistakably upbeat and constructive' :
analysis.sentimentScore > 60 ? 'generally warm and positive' :
analysis.sentimentScore > 45 ? 'balanced and even-handed' :
analysis.sentimentScore > 30 ? 'skeptical and critical by nature' : 'fiercely contrarian';
const ratioDesc = Number(analysis.ratio) > 20 ? 'a devoted commenter who rarely originates content' :
Number(analysis.ratio) > 8 ? 'someone who engages far more in discussion than in posting' :
Number(analysis.ratio) > 3 ? 'a balanced participant who both posts and comments' :
'a content creator at heart who posts far more than they comment';
const freqDesc = parseFloat(analysis.postsPerMonth) > 30 ? 'a relentless daily contributor' :
parseFloat(analysis.postsPerMonth) > 10 ? 'a highly active member' :
parseFloat(analysis.postsPerMonth) > 2 ? 'a regular contributor' : 'an occasional visitor';
let s = `${name} is a ${age}-old Redditor and ${freqDesc} with ${formatNumber(total)} total karma`;
if (top3.length) s += `, who calls ${top3.slice(0,-1).join(', ')}${top3.length > 1 ? ' and ' + top3[top3.length-1] : top3[0]} home`;
s += `. Across ${formatNumber(posts.length)} posts and ${formatNumber(comments.length)} comments, they emerge as ${ratioDesc}`;
if (analysis.avgPostScore > 0) s += `, achieving ${engagement} engagement with an average of ${formatNumber(analysis.avgPostScore)} upvotes per post`;
s += `. Their voice is ${sentDesc}`;
if (topW.length) s += `, and their writing gravitates toward themes of "${topW.join('", "')}"`;
s += `. Most at home during ${HOURS[analysis.peakHour]}`;
if (analysis.peakDow !== undefined) s += ` — especially on ${DAYS_FULL[analysis.peakDow]}`;
s += ` — their Reddit identity aligns with the "${analysis.persona.label}" archetype: ${analysis.persona.desc.toLowerCase()}.`;
return s;
}
function buildBehavioralInsight(profile, posts, comments, analysis) {
const total = (profile.link_karma||0) + (profile.comment_karma||0);
const lines = [];
// Engagement pattern
if (analysis.avgPostScore > 500) {
lines.push(`With an average of ${formatNumber(analysis.avgPostScore)} upvotes per post, this user has cracked the code on what resonates with Reddit communities — a rare skill that puts them in the top tier of content creators.`);
} else if (analysis.avgPostScore > 50) {
lines.push(`Their posts reliably accumulate upvotes above the community average, suggesting a good read on audience taste and consistent quality output.`);
} else {
lines.push(`Their posting style prioritizes participation over virality — the mark of someone who's here for the community conversation rather than the karma chase.`);
}
// Posting cadence insight
const freq = parseFloat(analysis.postsPerMonth);
if (freq > 30) {
lines.push(`Posting multiple times daily, they are deeply embedded in Reddit's real-time culture — this level of consistency suggests Reddit is a primary media outlet for them.`);
} else if (freq > 5) {
lines.push(`Their steady cadence of ~${Math.round(freq)} posts/month reflects an engaged but intentional approach — they show up regularly without over-saturating their audience.`);
}
// Controversy / sentiment angle
if (analysis.controversiality > 30) {
lines.push(`A controversiality rate of ${analysis.controversiality}% reveals a user who doesn't shy away from divisive takes — they either love debate or simply aren't optimizing for consensus.`);
}
if (analysis.sentimentScore > 75) {
lines.push(`Remarkably, ${analysis.sentimentScore}% of their language skews positive — in a platform often associated with cynicism, they stand out as a genuine force for good vibes.`);
}
// Community breadth
if (analysis.topSubs.length >= 6) {
lines.push(`Scattered across ${analysis.topSubs.length} distinct communities, their interests span a wide terrain — this breadth of engagement suggests intellectual curiosity and social versatility.`);
} else if (analysis.topSubs.length === 1) {
lines.push(`Nearly all their activity concentrates in a single subreddit — a deep specialist whose Reddit experience is tightly focused.`);
}
// Awards
if (analysis.awards > 20) {
lines.push(`${analysis.awards} awards accumulated across their history confirm that the community recognizes genuine value in their contributions.`);
}
return lines.slice(0, 3).join(' ');
}
function renderPersonalityBars(profile, posts, comments, analysis) {
const wrap = $('personalityBars');
if (!wrap) return;
const total = (profile.link_karma||0) + (profile.comment_karma||0);
// Define personality dimensions with calculated values
const dims = [
{
label: 'Positivity',