'use strict';
// ── Guard: stop cleanly if no data ──
const _store = SM.loadData();
if (!_store) {
window.location.replace('upload');
throw new Error('No data — redirecting');
}
SM.injectLayout('nav-analytics');
document.addEventListener('DOMContentLoaded', function () {
SM.setChartDefaults();
const { rows, meta } = _store;
document.getElementById('topbarMeta').textContent =
`${meta.filename} — ${rows.length} tweets`;
const n = rows.length;
const pos = rows.filter(r => r.sentiment === 'Positif');
const neg = rows.filter(r => r.sentiment === 'Negatif');
const neu = rows.filter(r => r.sentiment === 'Netral');
const {C, gridColor: G} = SM;
function safe(id, fn) {
try { fn(); }
catch(e) { console.error('Chart/section ' + id + ' failed:', e); }
}
// ── 1. DONUT ──
safe('c1', () => {
SM.mkChart('c1', {
type: 'doughnut',
data: {
labels: ['Positif','Negatif','Netral'],
datasets: [{ data:[pos.length,neg.length,neu.length],
backgroundColor:[C.pos,C.neg,C.neu], borderWidth:0, hoverOffset:12 }]
},
options: { responsive:true, maintainAspectRatio:false, cutout:'68%',
plugins:{ legend:{display:false},
tooltip:{callbacks:{label: c => ` ${c.label}: ${c.parsed} (${((c.parsed/n)*100).toFixed(1)}%)`}}}}
});
document.getElementById('legend1').innerHTML =
[['Positif',C.pos,pos.length],['Negatif',C.neg,neg.length],['Netral',C.neu,neu.length]]
.map(([l,c,v]) => `
`).join('');
});
// ── 2. TIME TREND ──
safe('c2', () => {
const tMap = {};
rows.forEach(r => {
if (!r.date) return;
const d = new Date(r.date);
const k = isNaN(d.getTime()) ? r.date.slice(0,10) : `${String(d.getHours()).padStart(2,'0')}:00`;
if (!tMap[k]) tMap[k] = {Positif:0, Negatif:0, Netral:0};
tMap[k][r.sentiment]++;
});
const tL = Object.keys(tMap).sort();
SM.mkChart('c2', {
type: 'line',
data: { labels: tL, datasets: [
{label:'Positif',data:tL.map(k=>tMap[k].Positif),borderColor:C.pos,backgroundColor:C.posDim,tension:.4,fill:true,pointRadius:3},
{label:'Negatif',data:tL.map(k=>tMap[k].Negatif),borderColor:C.neg,backgroundColor:C.negDim,tension:.4,fill:true,pointRadius:3},
{label:'Netral', data:tL.map(k=>tMap[k].Netral), borderColor:C.neu,backgroundColor:C.neuDim,tension:.4,fill:true,pointRadius:3},
]},
options: { responsive:true, maintainAspectRatio:false,
interaction:{mode:'index',intersect:false},
plugins:{legend:{labels:{boxWidth:10,padding:14}}},
scales:{ x:{grid:{color:G}}, y:{grid:{color:G},beginAtZero:true} }
}
});
});
// ── 3. STACKED TOPIC BAR ──
safe('c3', () => {
const topicMap = {};
rows.forEach(r => {
const t = r.raw.split(/[.!?]/)[0].trim().slice(0,45) || 'Lainnya';
if (!topicMap[t]) topicMap[t] = {Positif:0, Negatif:0, Netral:0, tot:0};
topicMap[t][r.sentiment]++; topicMap[t].tot++;
});
const topTopics = Object.entries(topicMap).sort((a,b) => b[1].tot-a[1].tot).slice(0,8);
SM.mkChart('c3', {
type: 'bar',
data: {
labels: topTopics.map(([k]) => k.length>38 ? k.slice(0,38)+'…' : k),
datasets: [
{label:'Positif',data:topTopics.map(([,v])=>v.Positif),backgroundColor:C.pos,borderRadius:3},
{label:'Negatif',data:topTopics.map(([,v])=>v.Negatif),backgroundColor:C.neg,borderRadius:3},
{label:'Netral', data:topTopics.map(([,v])=>v.Netral), backgroundColor:C.neu,borderRadius:3},
]
},
options: { responsive:true, maintainAspectRatio:false,
plugins:{legend:{labels:{boxWidth:9,padding:12}}},
scales:{ x:{stacked:true,grid:{color:G},ticks:{maxRotation:30,font:{size:9}}},
y:{stacked:true,grid:{color:G},beginAtZero:true} }
}
});
});
// ── 4. LOCATION HORIZ ──
safe('c4', () => {
const locMap = {};
rows.forEach(r => {
const l = (r.location||'').trim()||'Tidak Diketahui';
locMap[l] = (locMap[l]||0)+1;
});
const topLoc = Object.entries(locMap).sort((a,b) => b[1]-a[1]).slice(0,8);
SM.mkChart('c4', {
type: 'bar',
data: {
labels: topLoc.map(([k]) => k),
datasets: [{ label:'Tweet', data: topLoc.map(([,v]) => v),
backgroundColor: topLoc.map((_,i) => C.palette[i % C.palette.length]),
borderWidth:0, borderRadius:4 }]
},
options: { indexAxis:'y', responsive:true, maintainAspectRatio:false,
plugins:{legend:{display:false}},
scales:{ x:{grid:{color:G},beginAtZero:true}, y:{grid:{display:false}} }
}
});
});
// ── 5. TOP USERS ──
safe('c5', () => {
const userMap = {};
rows.forEach(r => { userMap[r.username] = (userMap[r.username]||0)+1; });
const topUsers = Object.entries(userMap).sort((a,b) => b[1]-a[1]).slice(0,8);
SM.mkChart('c5', {
type: 'bar',
data: {
labels: topUsers.map(([k]) => '@'+k),
datasets: [{ label:'Tweet', data: topUsers.map(([,v]) => v),
backgroundColor:C.a1d, borderColor:C.a1, borderWidth:1, borderRadius:4 }]
},
options: { indexAxis:'y', responsive:true, maintainAspectRatio:false,
plugins:{legend:{display:false}},
scales:{ x:{grid:{color:G},beginAtZero:true}, y:{grid:{display:false}} }
}
});
});
// ── 6. GROUPED CONFIDENCE per TOPIC ──
safe('c8', () => {
const ts = {};
rows.forEach(r => {
const t = r.raw.split(/[.!?]/)[0].trim().slice(0,35) || 'Lainnya';
if (!ts[t]) ts[t] = {pos:[],neg:[],neu:[]};
if (r.sentiment==='Positif') ts[t].pos.push(r.confidence);
else if (r.sentiment==='Negatif') ts[t].neg.push(r.confidence);
else ts[t].neu.push(r.confidence);
});
const ts6 = Object.entries(ts)
.sort((a,b) => (b[1].pos.length+b[1].neg.length+b[1].neu.length)-(a[1].pos.length+a[1].neg.length+a[1].neu.length))
.slice(0,6);
SM.mkChart('c8', {
type: 'bar',
data: {
labels: ts6.map(([k]) => k.length>28 ? k.slice(0,28)+'…' : k),
datasets: [
{label:'Positif',data:ts6.map(([,v]) => v.pos.length ? +(SM.avg(v.pos)*100).toFixed(1) : 0),backgroundColor:C.pos,borderRadius:3,borderWidth:0},
{label:'Negatif',data:ts6.map(([,v]) => v.neg.length ? +(SM.avg(v.neg)*100).toFixed(1) : 0),backgroundColor:C.neg,borderRadius:3,borderWidth:0},
{label:'Netral', data:ts6.map(([,v]) => v.neu.length ? +(SM.avg(v.neu)*100).toFixed(1) : 0),backgroundColor:C.neu,borderRadius:3,borderWidth:0},
]
},
options: { responsive:true, maintainAspectRatio:false,
plugins:{legend:{labels:{boxWidth:9,padding:12}}},
scales:{ x:{grid:{color:G},ticks:{maxRotation:30,font:{size:9}}},
y:{grid:{color:G},beginAtZero:true,max:100,ticks:{callback: v => v+'%'}} }
}
});
});
// ── 7. HOURLY ACTIVITY BAR ──
safe('c12', () => {
const hourly = Array(24).fill(0);
rows.forEach(r => {
if (!r.date) return;
const d = new Date(r.date);
if (!isNaN(d.getTime())) hourly[d.getHours()]++;
});
SM.mkChart('c12', {
type: 'bar',
data: {
labels: Array.from({length:24}, (_,i) => `${String(i).padStart(2,'0')}:00`),
datasets: [{ label:'Tweet', data: hourly,
backgroundColor: hourly.map((_,i) => i>=7 && i<=21 ? C.a1d : C.a2d),
borderColor: hourly.map((_,i) => i>=7 && i<=21 ? C.a1 : C.a2),
borderWidth:1, borderRadius:3 }]
},
options: { responsive:true, maintainAspectRatio:false,
plugins:{ legend:{display:false},
tooltip:{callbacks:{title: c=>`Jam ${c[0].label}`, label: c=>` ${c.parsed.y} tweet`}}},
scales:{ x:{grid:{color:G},ticks:{font:{size:9}}}, y:{grid:{color:G},beginAtZero:true} }
}
});
});
// ══════════════════════════════════════════
// ── 8. TOP 10 HASHTAG TABLE ──
// ══════════════════════════════════════════
safe('tblHashtag', () => {
const htMap = {};
rows.forEach(r => {
const tags = (r.raw.match(/#(\w+)/g) || []).map(t => t.toLowerCase());
tags.forEach(t => { htMap[t] = (htMap[t]||0)+1; });
});
const top10 = Object.entries(htMap).sort((a,b) => b[1]-a[1]).slice(0,10);
const tbody = document.getElementById('tblHashtagBody');
if (top10.length === 0) {
document.getElementById('tblHashtagEmpty').style.display = 'block';
return;
}
const maxV = top10[0][1];
tbody.innerHTML = top10.map(([tag, cnt], i) => `
| ${i+1} |
${SM.esc(tag)} |
${cnt} |
|
`).join('');
});
// ══════════════════════════════════════════
// ── 9. TOP 10 TOPIC TABLE ──
// ══════════════════════════════════════════
safe('tblTopic', () => {
const topicMap = {};
rows.forEach(r => {
const t = r.raw.split(/[.!?]/)[0].trim().slice(0,60) || 'Lainnya';
if (!topicMap[t]) topicMap[t] = {pos:0,neg:0,neu:0,tot:0};
topicMap[t][r.sentiment==='Positif'?'pos':r.sentiment==='Negatif'?'neg':'neu']++;
topicMap[t].tot++;
});
const top10 = Object.entries(topicMap).sort((a,b) => b[1].tot-a[1].tot).slice(0,10);
const tbody = document.getElementById('tblTopicBody');
tbody.innerHTML = top10.map(([topic, v], i) => `
| ${i+1} |
${SM.esc(topic.length>55?topic.slice(0,55)+'…':topic)} |
${v.tot} |
${v.pos} |
${v.neg} |
${v.neu} |
`).join('');
});
// ══════════════════════════════════════════
// ── 10. SENTIMENT PER HASHTAG TABLE ──
// ══════════════════════════════════════════
safe('tblHashtagSent', () => {
const htSent = {};
rows.forEach(r => {
const tags = (r.raw.match(/#(\w+)/g) || []).map(t => t.toLowerCase());
tags.forEach(tag => {
if (!htSent[tag]) htSent[tag] = {pos:0,neg:0,neu:0,tot:0};
htSent[tag][r.sentiment==='Positif'?'pos':r.sentiment==='Negatif'?'neg':'neu']++;
htSent[tag].tot++;
});
});
const top10 = Object.entries(htSent).sort((a,b) => b[1].tot-a[1].tot).slice(0,10);
const tbody = document.getElementById('tblHashtagSentBody');
if (top10.length === 0) {
document.getElementById('tblHashtagSentEmpty').style.display = 'block';
return;
}
tbody.innerHTML = top10.map(([tag, v], i) => {
const dominated = v.pos>=v.neg && v.pos>=v.neu ? 'Positif' :
v.neg>v.pos && v.neg>=v.neu ? 'Negatif' : 'Netral';
const domColor = dominated==='Positif'?'var(--pos)':dominated==='Negatif'?'var(--neg)':'var(--neu)';
const pPct = (v.pos/v.tot*100).toFixed(0);
const nPct = (v.neg/v.tot*100).toFixed(0);
const neuPct = (100 - +pPct - +nPct);
return `
| ${i+1} |
${SM.esc(tag)} |
${v.tot} |
${v.pos} |
${v.neg} |
${v.neu} |
${dominated} |
|
`;
}).join('');
});
// ══════════════════════════════════════════
// ── 11. LANGUAGE CHART ──
// ══════════════════════════════════════════
safe('cLang', () => {
const langMap = {};
const LANG_NAMES = {
'in':'Indonesia','id':'Indonesia','en':'English','ms':'Malay',
'ar':'Arab','zh':'Chinese','ja':'Japanese','ko':'Korean',
'fr':'French','de':'German','es':'Spanish','pt':'Portuguese','nl':'Dutch','tr':'Turkish',
'tl':'Filipino','th':'Thai','vi':'Vietnamese','hi':'Hindi','und':'Lainnya'
};
rows.forEach(r => {
const l = (r.lang||'und').toLowerCase();
const name = LANG_NAMES[l] || l.toUpperCase();
if (!langMap[name]) langMap[name] = {Positif:0,Negatif:0,Netral:0,tot:0};
langMap[name][r.sentiment]++; langMap[name].tot++;
});
const topLangs = Object.entries(langMap).sort((a,b) => b[1].tot-a[1].tot).slice(0,12);
SM.mkChart('cLang', {
type:'bar',
data:{
labels:topLangs.map(([l])=>l),
datasets:[
{label:'Positif',data:topLangs.map(([,v])=>v.Positif),backgroundColor:C.pos,borderRadius:4,borderWidth:0},
{label:'Negatif',data:topLangs.map(([,v])=>v.Negatif),backgroundColor:C.neg,borderRadius:4,borderWidth:0},
{label:'Netral', data:topLangs.map(([,v])=>v.Netral), backgroundColor:C.neu,borderRadius:4,borderWidth:0},
]
},
options:{responsive:true,maintainAspectRatio:false,
plugins:{legend:{labels:{boxWidth:9,padding:14}}},
scales:{
x:{stacked:true,grid:{color:G}},
y:{stacked:true,grid:{color:G},beginAtZero:true}
}
}
});
});
// ══════════════════════════════════════════
// ── 12–14. COMMON WORDS PANELS ──
// ══════════════════════════════════════════
function getTopWords(subset, topN = 30) {
const freq = {};
subset.forEach(r => {
const words = (r.cleaned || r.raw).toLowerCase()
.replace(/[^a-z0-9\s]/g,' ').split(/\s+/)
.filter(w => w.length > 2 && !SM.STOPWORDS.has(w));
words.forEach(w => { freq[w] = (freq[w]||0)+1; });
});
return Object.entries(freq).sort((a,b) => b[1]-a[1]).slice(0,topN);
}
function renderWordTags(containerId, words, baseColor) {
const el = document.getElementById(containerId);
if (!el) return;
if (!words.length) {
el.innerHTML = `
`;
return;
}
const maxF = words[0][1];
el.innerHTML = words.map(([w, f]) => {
const size = 11 + Math.round((f/maxF)*10);
const alpha = 0.35 + (f/maxF)*0.65;
return `${SM.esc(w)}`;
}).join('');
}
safe('wordsPos', () => renderWordTags('wordsPos', getTopWords(pos), C.pos));
safe('wordsNeu', () => renderWordTags('wordsNeu', getTopWords(neu), C.neu));
safe('wordsNeg', () => renderWordTags('wordsNeg', getTopWords(neg), C.neg));
}); // end DOMContentLoaded