'use strict';
// ── Guard BEFORE DOM ──
const _store = SM.loadData();
if (!_store) {
window.location.replace('upload');
throw new Error('No data — redirecting');
}
SM.injectLayout('nav-dashboard');
document.addEventListener('DOMContentLoaded', function () {
SM.setChartDefaults();
const { rows, meta } = _store;
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 n = rows.length;
const totalEngage = rows.reduce((s,r) => s + r.engagement, 0);
const avgConf = SM.avg(rows.map(r => r.confidence));
const sentScore = +((pos.length - neg.length) / n).toFixed(3);
// Topbar
document.getElementById('topbarMeta').textContent = `${meta.filename} — ${n} tweets`;
if (meta.dateMin) {
const d1 = new Date(meta.dateMin), d2 = new Date(meta.dateMax);
document.getElementById('datePeriod').textContent =
`${isNaN(d1.getTime())?meta.dateMin:d1.toLocaleDateString('id-ID')} – ${isNaN(d2.getTime())?meta.dateMax:d2.toLocaleDateString('id-ID')}`;
}
// ── KPI Cards ──
document.getElementById('kpiGrid').innerHTML = [
{ color:'blue', label:'Total Tweet', value: SM.fmt(n), sub: meta.filename },
{ color:'green', label:'Positif', value: SM.fmt(pos.length), sub: `${((pos.length/n)*100).toFixed(1)}% dari total` },
{ color:'red', label:'Negatif', value: SM.fmt(neg.length), sub: `${((neg.length/n)*100).toFixed(1)}% dari total` },
{ color:'yellow', label:'Netral', value: SM.fmt(neu.length), sub: `${((neu.length/n)*100).toFixed(1)}% dari total` },
{ color:'cyan', label:'Total Engagement', value: SM.fmt(totalEngage), sub: 'Like + RT + Reply + Quote' },
{ color:'purple', label:'Avg Engagement', value: SM.fmt(Math.round(totalEngage/n)), sub: 'Per tweet' },
{ color:'pink', label:'Avg Kepercayaan', value: (avgConf*100).toFixed(1)+'%', sub: 'Skor confidence model' },
{ color:'orange', label:'Skor Sentimen',
value: sentScore >= 0 ? '+'+sentScore : String(sentScore),
sub: 'Skala −1.0 hingga +1.0',
delta: sentScore > 0.2 ? 'up' : sentScore < -0.2 ? 'down' : 'mid',
deltaText: sentScore > 0.2 ? 'Cenderung Positif' : sentScore < -0.2 ? 'Cenderung Negatif' : 'Cenderung Netral' },
].map(k => `
${k.label}
${k.value}
${k.sub}
${k.delta ? `
${k.deltaText}
` : ''}
`).join('');
// ── Gauge ──
try {
const canvas = document.getElementById('chartGauge');
if (canvas) {
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const pct = (sentScore + 1) / 2;
const startA = Math.PI, angle = startA + pct * Math.PI;
const cx = W/2, cy = H - 10, r = 100;
ctx.clearRect(0,0,W,H);
ctx.beginPath(); ctx.arc(cx,cy,r,startA,2*Math.PI);
ctx.lineWidth=18; ctx.strokeStyle='rgba(255,255,255,0.06)'; ctx.lineCap='round'; ctx.stroke();
const g = ctx.createLinearGradient(cx-r,cy,cx+r,cy);
g.addColorStop(0,'#f87171'); g.addColorStop(0.5,'#fbbf24'); g.addColorStop(1,'#34d399');
ctx.beginPath(); ctx.arc(cx,cy,r,startA,angle);
ctx.lineWidth=18; ctx.strokeStyle=g; ctx.lineCap='round'; ctx.stroke();
const nx=cx+(r-9)*Math.cos(angle), ny=cy+(r-9)*Math.sin(angle);
ctx.beginPath(); ctx.moveTo(cx,cy); ctx.lineTo(nx,ny);
ctx.lineWidth=2; ctx.strokeStyle='#e8eaf0'; ctx.lineCap='round'; ctx.stroke();
ctx.beginPath(); ctx.arc(cx,cy,5,0,2*Math.PI); ctx.fillStyle='#e8eaf0'; ctx.fill();
ctx.font='10px Inter'; ctx.fillStyle='#525b72'; ctx.textAlign='center';
ctx.fillText('−1.0',cx-r+6,cy+16); ctx.fillText('0',cx,cy-r-6); ctx.fillText('+1.0',cx+r-6,cy+16);
}
document.getElementById('gaugeLabel').textContent =
`Skor: ${sentScore>=0?'+':''}${sentScore} — ${
sentScore>0.3?'Sangat Positif':sentScore>0.05?'Positif':sentScore>-0.05?'Netral':sentScore>-0.3?'Negatif':'Sangat Negatif'}`;
} catch(e) { console.error('Gauge error:', e); }
// ── Donut ──
try {
SM.mkChart('chartDonut', {
type: 'doughnut',
data: { labels:['Positif','Negatif','Netral'],
datasets:[{ data:[pos.length,neg.length,neu.length],
backgroundColor:[SM.C.pos,SM.C.neg,SM.C.neu], borderWidth:0, hoverOffset:8 }] },
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('legendDonut').innerHTML =
[['Positif',SM.C.pos,pos.length],['Negatif',SM.C.neg,neg.length],['Netral',SM.C.neu,neu.length]]
.map(([l,c,v])=>``).join('');
} catch(e) { console.error('Donut error:', e); }
// ── Time trend ──
try {
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('chartTrend', {
type:'line',
data:{ labels:tL, datasets:[
{label:'Positif',data:tL.map(k=>tMap[k].Positif),borderColor:SM.C.pos,backgroundColor:SM.C.posDim,tension:.4,fill:true,pointRadius:3},
{label:'Negatif',data:tL.map(k=>tMap[k].Negatif),borderColor:SM.C.neg,backgroundColor:SM.C.negDim,tension:.4,fill:true,pointRadius:3},
{label:'Netral', data:tL.map(k=>tMap[k].Netral), borderColor:SM.C.neu,backgroundColor:SM.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:SM.gridColor}}, y:{grid:{color:SM.gridColor},beginAtZero:true} }
}
});
} catch(e) { console.error('Trend error:', e); }
// ── Top tweets ──
function tweetCard(r, cls) {
return ``;
}
const topPos = [...pos].sort((a,b)=>b.confidence-a.confidence).slice(0,5);
const topNeg = [...neg].sort((a,b)=>b.confidence-a.confidence).slice(0,5);
const emptyPos = `
Tidak Ada Data
Belum ada tweet dengan sentimen positif.
`;
const emptyNeg = `
Tidak Ada Data
Belum ada tweet dengan sentimen negatif.
`;
document.getElementById('positiveTweets').innerHTML = topPos.map(r=>tweetCard(r,'pos')).join('') || emptyPos;
document.getElementById('negativeTweets').innerHTML = topNeg.map(r=>tweetCard(r,'neg')).join('') || emptyNeg;
// ── Leaderboard ──
const userEng = {};
rows.forEach(r => { userEng[r.username] = (userEng[r.username]||0) + r.engagement; });
document.getElementById('leaderboard').innerHTML =
Object.entries(userEng).sort((a,b)=>b[1]-a[1]).slice(0,10)
.map(([u,e],i) => `
${i+1}.
@${SM.esc(u)}
${SM.fmt(e)} eng.
`).join('');
// ── Insights ──
const posRatio = (pos.length/n*100).toFixed(1), negRatio = (neg.length/n*100).toFixed(1);
const highConf = rows.filter(r=>r.confidence>0.85).length;
const locCounts = {}; rows.forEach(r => { locCounts[r.location]=(locCounts[r.location]||0)+1; });
const topLoc = Object.entries(locCounts).sort((a,b)=>b[1]-a[1])[0];
const posConf = pos.length ? SM.avg(pos.map(r=>r.confidence)) : 0;
const negConf = neg.length ? SM.avg(neg.map(r=>r.confidence)) : 0;
document.getElementById('insights').innerHTML = [
{ color: pos.length>neg.length ? SM.C.pos : SM.C.neg,
text: `${pos.length>neg.length?'Mayoritas sentimen Positif':'Sentimen Negatif dominan'} (${posRatio}% vs ${negRatio}%).` },
{ color: SM.C.a1,
text: `${highConf} tweet (${((highConf/n)*100).toFixed(1)}%) kepercayaan model di atas 85%.` },
...(topLoc ? [{ color:SM.C.a2, text:`Lokasi paling aktif: ${topLoc[0]} dengan ${topLoc[1]} tweet.` }]:[]),
{ color: SM.C.a3, text:`Rata-rata engagement per tweet: ${SM.fmt(Math.round(totalEngage/n))}.` },
{ color: SM.C.a4, text:`Avg kepercayaan — Positif: ${(posConf*100).toFixed(1)}%, Negatif: ${(negConf*100).toFixed(1)}%.` },
].map(ins => ``).join('');
});