sentimeter / js /data.js
rhmnsae's picture
up
3cc3895
'use strict';
SM.injectLayout('nav-data');
const store = SM.loadData();
if (!store) {
window.location.replace('upload');
throw new Error('No data β€” redirecting to upload');
}
const { rows, meta } = store;
document.getElementById('topbarMeta').textContent = `${meta.filename} β€” ${rows.length} tweets`;
// ── Column definitions ──
const COLS = [
{ key:'id', label:'No', visible:true, sortable:true },
{ key:'raw', label:'Teks Asli', visible:true, sortable:false },
{ key:'cleaned', label:'Teks Bersih', visible:false, sortable:false },
{ key:'username', label:'Username', visible:true, sortable:true },
{ key:'location', label:'Lokasi', visible:true, sortable:true },
{ key:'date', label:'Waktu', visible:true, sortable:true },
{ key:'sentiment', label:'Sentimen', visible:true, sortable:true },
{ key:'confidence', label:'Kepercayaan', visible:true, sortable:true },
{ key:'fav', label:'Like', visible:true, sortable:true },
{ key:'rt', label:'Retweet', visible:true, sortable:true },
{ key:'rep', label:'Reply', visible:false, sortable:true },
{ key:'qot', label:'Quote', visible:false, sortable:true },
{ key:'engagement', label:'Engagement', visible:true, sortable:true },
];
// Populate location & user dropdowns
const locs = [...new Set(rows.map(r=>(r.location||'').trim()||'β€”'))].sort();
const users = [...new Set(rows.map(r=>r.username))].sort();
const fLoc = document.getElementById('fLocation');
const fUser = document.getElementById('fUser');
locs.forEach(l => { const o=document.createElement('option'); o.value=l; o.textContent=l; fLoc.appendChild(o); });
users.forEach(u => { const o=document.createElement('option'); o.value=u; o.textContent='@'+u; fUser.appendChild(o); });
// Column toggle buttons
document.getElementById('colToggle').innerHTML = COLS.map((c,i) =>
`<div class="col-pill ${c.visible?'on':''}" data-colidx="${i}">${c.label}</div>`
).join('');
document.querySelectorAll('.col-pill').forEach(pill => {
pill.addEventListener('click', () => {
const i = +pill.dataset.colidx;
COLS[i].visible = !COLS[i].visible;
pill.classList.toggle('on', COLS[i].visible);
renderTable();
});
});
// ── State ──
let filtered = [...rows];
let currentPage = 1;
let pageSize = 20;
let sortKey = 'id', sortDir = 1;
let expandedRows = new Set();
// ── Filters ──
function applyFilters() {
const s = document.getElementById('fSentiment').value;
const loc = document.getElementById('fLocation').value;
const usr = document.getElementById('fUser').value;
const q = document.getElementById('fSearch').value.toLowerCase().trim();
const minE= parseInt(document.getElementById('fMinEngage').value)||0;
const minC= (parseFloat(document.getElementById('fMinConf').value)||0)/100;
filtered = rows.filter(r =>
(s==='all' || r.sentiment===s) &&
(loc==='all' || (r.location||'β€”')===loc) &&
(usr==='all' || r.username===usr) &&
(r.engagement >= minE) &&
(r.confidence >= minC) &&
(!q || r.raw.toLowerCase().includes(q) || r.username.toLowerCase().includes(q) || r.cleaned.toLowerCase().includes(q))
);
// Sort
filtered.sort((a,b)=>{
const va=a[sortKey], vb=b[sortKey];
if(typeof va==='number') return (va-vb)*sortDir;
return String(va).localeCompare(String(vb))*sortDir;
});
currentPage = 1;
renderTable();
}
['fSentiment','fLocation','fUser'].forEach(id => document.getElementById(id).addEventListener('change', applyFilters));
document.getElementById('fSearch').addEventListener('input', applyFilters);
document.getElementById('fMinEngage').addEventListener('input', applyFilters);
document.getElementById('fMinConf').addEventListener('input', applyFilters);
document.getElementById('pageSize').addEventListener('change', e => { pageSize=+e.target.value; currentPage=1; renderTable(); });
document.getElementById('btnReset').addEventListener('click', () => {
document.getElementById('fSentiment').value='all';
document.getElementById('fLocation').value='all';
document.getElementById('fUser').value='all';
document.getElementById('fSearch').value='';
document.getElementById('fMinEngage').value='';
document.getElementById('fMinConf').value='';
// Refresh custom select labels after reset
['fSentiment','fLocation','fUser','pageSize'].forEach(id => {
const el = document.getElementById(id);
if (el) el.dispatchEvent(new Event('_csdRefresh'));
});
applyFilters();
});
// ── Table HEAD ──
function renderHead() {
const visCols = COLS.filter(c=>c.visible);
document.getElementById('tableHead').innerHTML = `<tr>
${visCols.map(c => {
const isSorted = sortKey === c.key;
const sortIcon = isSorted
? (sortDir === 1
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" style="width:12px;height:12px;margin-left:4px"><polyline points="18 15 12 9 6 15"/></svg>'
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" style="width:12px;height:12px;margin-left:4px"><polyline points="6 9 12 15 18 9"/></svg>')
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:12px;height:12px;margin-left:4px;opacity:0.2"><polyline points="6 9 12 15 18 9"/></svg>';
return `<th ${c.sortable ? `data-sort="${c.key}" class="${isSorted ? 'active-sort' : ''}"` : ''}>
<div style="display:flex; align-items:center; justify-content:space-between">
${c.label}
${c.sortable ? sortIcon : ''}
</div>
</th>`;
}).join('')}
</tr>`;
document.querySelectorAll('th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
if(sortKey===th.dataset.sort) sortDir*=-1;
else { sortKey=th.dataset.sort; sortDir=-1; }
applyFilters(); // Use applyFilters to trigger sorting before re-render
});
});
}
// ── Table BODY ──
function renderTable() {
renderHead();
const visCols = COLS.filter(c=>c.visible);
const start = (currentPage-1)*pageSize;
const page = filtered.slice(start, start+pageSize);
document.getElementById('tableBody').innerHTML = page.map(r => {
const cells = visCols.map(c => {
const v = r[c.key];
if(c.key==='sentiment') return `<td><span class="badge badge-${r.sentiment==='Positif'?'pos':r.sentiment==='Negatif'?'neg':'neu'}">${r.sentiment}</span></td>`;
if(c.key==='confidence') return `<td>
<div class="conf-row">
<span class="conf-num">${(v*100).toFixed(1)}%</span>
<div class="conf-track"><div class="conf-fill conf-${r.sentiment==='Positif'?'pos':r.sentiment==='Negatif'?'neg':'neu'}" style="width:${(v*100).toFixed(0)}%"></div></div>
</div></td>`;
if(c.key==='raw') return `<td class="td-trunc" title="${SM.esc(v)}">${SM.esc(v.slice(0,80))}${v.length>80?'…':''}</td>`;
if(c.key==='cleaned') return `<td class="td-trunc" title="${SM.esc(v)}">${SM.esc(v.slice(0,60))}${v.length>60?'…':''}</td>`;
if(c.key==='date') {
const d=new Date(v); return `<td class="td-trunc-sm">${isNaN(d)?v:d.toLocaleTimeString('id-ID',{hour:'2-digit',minute:'2-digit'})}</td>`;
}
if(c.key==='username') return `<td style="font-weight:500;color:var(--tx1);white-space:nowrap">@${SM.esc(v)}</td>`;
if(c.key==='id') return `<td class="td-no">${v}</td>`;
return `<td>${SM.esc(String(v))}</td>`;
}).join('');
return `<tr data-rowid="${r.id}">
${cells}
</tr>`;
}).join('');
renderPagination();
document.getElementById('tableInfo').textContent =
`Menampilkan ${Math.min(start+1,filtered.length)}–${Math.min(start+pageSize,filtered.length)} dari ${filtered.length} data (total ${rows.length})`;
}
function renderPagination() {
const total = Math.ceil(filtered.length/pageSize);
const pg = document.getElementById('pagination');
if(total<=1) { pg.innerHTML=''; return; }
const range=[1];
if(currentPage>3) range.push('…');
for(let i=Math.max(2,currentPage-1);i<=Math.min(total-1,currentPage+1);i++) range.push(i);
if(currentPage<total-2) range.push('…');
if(total>1) range.push(total);
pg.innerHTML = `
<button class="pg-btn" ${currentPage===1?'disabled':''} onclick="goPage(${currentPage-1})" title="Previous">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px"><polyline points="15 18 9 12 15 6"/></svg>
</button>
${range.map(p=>p==='…'?`<span class="pg-btn dots">…</span>`
:`<button class="pg-btn ${p===currentPage?'active':''}" onclick="goPage(${p})">${p}</button>`).join('')}
<button class="pg-btn" ${currentPage===total?'disabled':''} onclick="goPage(${currentPage+1})" title="Next">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px"><polyline points="9 18 15 12 9 6"/></svg>
</button>`;
}
window.goPage = function(p) {
currentPage=Math.max(1,Math.min(p,Math.ceil(filtered.length/pageSize)));
expandedRows.clear(); renderTable();
document.getElementById('dataTable').scrollIntoView({behavior:'smooth'});
};
// Initial render
applyFilters();
// ── Custom Dropdowns ──
SM.initCustomSelect(document.getElementById('fSentiment'), {
showDots: {
'all': null,
'Positif': '#34d399',
'Negatif': '#f87171',
'Netral': '#fbbf24',
}
});
SM.initCustomSelect(document.getElementById('fLocation'));
SM.initCustomSelect(document.getElementById('fUser'));
SM.initCustomSelect(document.getElementById('pageSize'), { compact: true });
SM.initCustomNumber(document.getElementById('fMinEngage'));
SM.initCustomNumber(document.getElementById('fMinConf'));