'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])=>`
${l} (${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 `
${SM.esc(r.raw.slice(0,120))}${r.raw.length>120?'…':''}
@${SM.esc(r.username)}${r.fav} like ${r.rt} RT${(r.confidence*100).toFixed(0)}% conf.
`; } 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 => `
${ins.text}
`).join(''); });