MediaWiki:Common.js: Difference between revisions
No edit summary |
Add geo dashboard script: Users by Country with day/week/month periods, activity chart, world map |
||
| (One intermediate revision by the same user not shown) | |||
| Line 843: | Line 843: | ||
var apiBase = mw.util.wikiScript('api'); | var apiBase = mw.util.wikiScript('api'); | ||
var allRows = []; | var allRows = []; | ||
var tableBody = null; | |||
var statusSpan = null; | |||
$(document).ready(function() { | $(document).ready(function() { | ||
var wrapper = document.getElementById('article-overview-wrapper'); | var wrapper = document.getElementById('article-overview-wrapper'); | ||
if (!wrapper) return; | |||
if (!wrapper | |||
// | // Build controls row with button + status | ||
var | var controls = document.createElement('div'); | ||
controls.style.marginBottom = '14px'; | |||
var btn = document.createElement('button'); | var btn = document.createElement('button'); | ||
btn.textContent = 'Download CSV'; | btn.textContent = 'Download CSV'; | ||
btn.style.cssText = 'background-color:#e07b00;color:#fff;padding:9px 20px;border:none;border-radius:5px;font-size:14px;font-weight:bold;cursor:pointer;letter-spacing:0.3px;'; | btn.style.cssText = 'background-color:#e07b00;color:#fff;padding:9px 20px;border:none;border-radius:5px;font-size:14px;font-weight:bold;cursor:pointer;letter-spacing:0.3px;'; | ||
| Line 876: | Line 860: | ||
controls.appendChild(btn); | controls.appendChild(btn); | ||
statusSpan = document.createElement('span'); | |||
statusSpan.style.cssText = 'margin-left:14px;font-size:13px;color:#aaa;vertical-align:middle;'; | statusSpan.style.cssText = 'margin-left:14px;font-size:13px;color:#aaa;vertical-align:middle;'; | ||
statusSpan.textContent = 'Loading…'; | |||
controls.appendChild(statusSpan); | controls.appendChild(statusSpan); | ||
wrapper.appendChild(controls); | |||
// Build table | |||
var table = document.createElement('table'); | |||
table.style.cssText = 'width:100%;border-collapse:collapse;font-size:13px;'; | |||
var thead = document.createElement('thead'); | |||
var headerRow = document.createElement('tr'); | |||
headerRow.style.cssText = 'background-color:#2a2a2a;color:#f0a500;text-align:left;'; | |||
var cols = ['Category', 'Subcategory', 'Title', 'Words', 'Focus']; | |||
cols.forEach(function(colName, i) { | |||
var th = document.createElement('th'); | |||
th.textContent = colName; | |||
th.style.cssText = 'padding:9px 12px;border:1px solid #444;' + (colName === 'Words' ? 'text-align:right;' : ''); | |||
headerRow.appendChild(th); | |||
}); | |||
thead.appendChild(headerRow); | |||
table.appendChild(thead); | |||
tableBody = document.createElement('tbody'); | |||
tableBody.innerHTML = '<tr><td colspan="5" style="padding:12px;text-align:center;color:#aaa;font-style:italic;">Loading articles… please wait.</td></tr>'; | |||
table.appendChild(tableBody); | |||
wrapper.appendChild(table); | |||
buildTable(); | buildTable(); | ||
| Line 908: | Line 914: | ||
function renderTable(rows) { | function renderTable(rows) { | ||
if (!tableBody) return; | |||
if (! | |||
if (!rows || rows.length === 0) { | if (!rows || rows.length === 0) { | ||
tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#aaa;">No articles found.</td></tr>'; | |||
return; | return; | ||
} | } | ||
| Line 925: | Line 930: | ||
html += '</tr>'; | html += '</tr>'; | ||
}); | }); | ||
tableBody.innerHTML = html; | |||
} | } | ||
function buildTable() { | function buildTable() { | ||
if (statusSpan) statusSpan.textContent = 'Fetching categories…'; | |||
if ( | |||
$.getJSON(apiBase, { action:'query', list:'allcategories', aclimit:500, format:'json' }) | $.getJSON(apiBase, { action:'query', list:'allcategories', aclimit:500, format:'json' }) | ||
| Line 940: | Line 944: | ||
if (totalCats === 0) { | if (totalCats === 0) { | ||
if (tableBody) tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#aaa;">No categories found.</td></tr>'; | |||
if ( | if (statusSpan) statusSpan.textContent = ''; | ||
if ( | |||
return; | return; | ||
} | } | ||
| Line 948: | Line 951: | ||
function checkDone() { | function checkDone() { | ||
processed++; | processed++; | ||
if ( | if (statusSpan) statusSpan.textContent = 'Loading… (' + processed + '/' + totalCats + ' categories)'; | ||
if (processed === totalCats) { | if (processed === totalCats) { | ||
rows.sort(function(a,b){ | rows.sort(function(a,b){ | ||
| Line 959: | Line 962: | ||
allRows = rows; | allRows = rows; | ||
renderTable(rows); | renderTable(rows); | ||
if ( | if (statusSpan) statusSpan.textContent = rows.length + ' article entries loaded.'; | ||
} | } | ||
} | } | ||
| Line 998: | Line 1,001: | ||
}) | }) | ||
.fail(function() { | .fail(function() { | ||
if (tableBody) tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#f55;">Error loading data. Check API access.</td></tr>'; | |||
if ( | |||
}); | }); | ||
} | } | ||
| Line 1,023: | Line 1,025: | ||
} | } | ||
})(); | |||
// ═══════════════════════════════════════════════════════════ | |||
// GEO DASHBOARD – Users by Country (Day / Week / Month) | |||
// ═══════════════════════════════════════════════════════════ | |||
(function () { | |||
if (!document.getElementById('geo-dashboard')) return; | |||
var API = '/api.php'; | |||
function isoDate(d) { return d.toISOString().slice(0, 10); } | |||
function startOf(period) { | |||
var now = new Date(); | |||
if (period === 'day') return new Date(now.getFullYear(), now.getMonth(), now.getDate()); | |||
if (period === 'week') { var d = new Date(now); d.setDate(d.getDate() - d.getDay()); d.setHours(0,0,0,0); return d; } | |||
if (period === 'month') return new Date(now.getFullYear(), now.getMonth(), 1); | |||
return new Date(0); | |||
} | |||
var COUNTRY_MAP = { | |||
'en':'🇺🇸 English / US','de':'🇩🇪 German','fr':'🇫🇷 French','es':'🇪🇸 Spanish', | |||
'it':'🇮🇹 Italian','pt':'🇧🇷 Portuguese','ru':'🇷🇺 Russian','ja':'🇯🇵 Japanese', | |||
'zh':'🇨🇳 Chinese','ko':'🇰🇷 Korean','ar':'🇸🇦 Arabic','nl':'🇳🇱 Dutch', | |||
'pl':'🇵🇱 Polish','sv':'🇸🇪 Swedish','no':'🇳🇴 Norwegian','da':'🇩🇰 Danish', | |||
'fi':'🇫🇮 Finnish','tr':'🇹🇷 Turkish','cs':'🇨🇿 Czech','hu':'🇭🇺 Hungarian', | |||
'ro':'🇷🇴 Romanian','uk':'🇺🇦 Ukrainian','vi':'🇻🇳 Vietnamese','th':'🇹🇭 Thai', | |||
'id':'🇮🇩 Indonesian','ms':'🇲🇾 Malay','he':'🇮🇱 Hebrew','fa':'🇮🇷 Persian', | |||
'hi':'🇮🇳 Hindi','bn':'🇧🇩 Bengali' | |||
}; | |||
function fetchUsers() { | |||
return fetch(API + '?action=query&list=allusers&aulimit=500&format=json') | |||
.then(function(r) { return r.json(); }) | |||
.then(function(d) { return d.query.allusers || []; }); | |||
} | |||
function fetchChanges() { | |||
return fetch(API + '?action=query&list=recentchanges&rclimit=500&rcprop=user%7Ctimestamp&format=json') | |||
.then(function(r) { return r.json(); }) | |||
.then(function(d) { return d.query.recentchanges || []; }); | |||
} | |||
function fetchBabel(username) { | |||
return fetch(API + '?action=query&titles=User:' + encodeURIComponent(username) + '&prop=categories&format=json') | |||
.then(function(r) { return r.json(); }) | |||
.then(function(d) { | |||
var pages = (d.query && d.query.pages) ? d.query.pages : {}; | |||
var langs = []; | |||
Object.values(pages).forEach(function(p) { | |||
(p.categories || []).forEach(function(c) { | |||
var m = c.title.match(/Category:User ([a-z]{2,3})/i); | |||
if (m) langs.push(m[1].toLowerCase()); | |||
}); | |||
}); | |||
return langs; | |||
}); | |||
} | |||
function drawMap(activeLangs) { | |||
var svg = document.getElementById('geo-world-map'); | |||
if (!svg) return; | |||
svg.innerHTML = ''; | |||
var bg = document.createElementNS('http://www.w3.org/2000/svg','rect'); | |||
bg.setAttribute('width','900'); bg.setAttribute('height','440'); bg.setAttribute('fill','#0d1f33'); | |||
svg.appendChild(bg); | |||
var continents = [ | |||
'M80,100 L240,95 L255,200 L220,230 L170,235 L110,220 L75,170 Z', | |||
'M155,240 L220,235 L235,360 L195,385 L155,370 L138,320 Z', | |||
'M385,90 L480,85 L490,160 L450,175 L400,170 L378,140 Z', | |||
'M390,175 L460,170 L470,320 L430,345 L388,335 L375,280 Z', | |||
'M490,80 L750,75 L760,230 L700,250 L520,240 L485,180 Z', | |||
'M640,280 L740,275 L748,355 L700,368 L638,355 Z', | |||
'M230,50 L290,45 L295,90 L255,95 L228,82 Z' | |||
]; | |||
continents.forEach(function(d) { | |||
var path = document.createElementNS('http://www.w3.org/2000/svg','path'); | |||
path.setAttribute('d', d); path.setAttribute('fill','#2a3a2a'); | |||
path.setAttribute('stroke','#1a2a1a'); path.setAttribute('stroke-width','1'); | |||
svg.appendChild(path); | |||
}); | |||
var shapes = { | |||
'de':'M440,145 L455,140 L465,148 L462,165 L448,170 L438,162 Z', | |||
'fr':'M415,152 L430,148 L438,162 L430,175 L415,172 L408,162 Z', | |||
'es':'M390,170 L415,168 L415,185 L395,190 L385,182 Z', | |||
'it':'M450,168 L462,165 L468,180 L460,195 L450,188 L445,178 Z', | |||
'gb':'M418,135 L428,132 L430,145 L420,148 L414,142 Z', | |||
'ru':'M460,120 L560,110 L580,135 L540,145 L465,148 Z', | |||
'us':'M120,150 L220,148 L225,190 L200,200 L115,195 Z', | |||
'cn':'M620,150 L680,145 L685,175 L650,185 L615,178 Z', | |||
'jp':'M695,155 L708,152 L710,165 L700,168 L693,163 Z', | |||
'br':'M195,230 L240,225 L245,275 L215,282 L188,270 Z', | |||
'in':'M580,175 L615,170 L618,210 L595,218 L575,205 Z', | |||
'au':'M660,280 L720,275 L725,320 L690,328 L655,315 Z' | |||
}; | |||
Object.keys(shapes).forEach(function(cc) { | |||
var isActive = activeLangs.some(function(l) { return l.startsWith(cc); }); | |||
var path = document.createElementNS('http://www.w3.org/2000/svg','path'); | |||
path.setAttribute('d', shapes[cc]); | |||
path.setAttribute('fill', isActive ? '#ff6600' : '#3a4a3a'); | |||
path.setAttribute('stroke','#555'); path.setAttribute('stroke-width','1'); | |||
path.setAttribute('opacity', isActive ? '0.9' : '0.5'); | |||
svg.appendChild(path); | |||
}); | |||
var labels = [{x:160,y:175,t:'North America'},{x:450,y:130,t:'Europe'},{x:620,y:165,t:'Asia'}, | |||
{x:430,y:265,t:'Africa'},{x:192,y:305,t:'South America'},{x:693,y:325,t:'Oceania'}]; | |||
labels.forEach(function(loc) { | |||
var text = document.createElementNS('http://www.w3.org/2000/svg','text'); | |||
text.setAttribute('x',loc.x); text.setAttribute('y',loc.y); | |||
text.setAttribute('fill','#444'); text.setAttribute('font-size','10'); | |||
text.setAttribute('font-family','sans-serif'); text.setAttribute('text-anchor','middle'); | |||
text.textContent = loc.t; svg.appendChild(text); | |||
}); | |||
} | |||
function renderActivityBars(daily) { | |||
var wrap = document.getElementById('geo-activity-bars'); | |||
var labelWrap = document.getElementById('geo-activity-labels'); | |||
if (!wrap) return; | |||
var days = []; | |||
for (var i = 29; i >= 0; i--) { | |||
var d = new Date(); d.setDate(d.getDate() - i); days.push(isoDate(d)); | |||
} | |||
var max = Math.max.apply(null, days.map(function(d){ return daily[d]||0; }).concat([1])); | |||
var bHtml = '', lHtml = ''; | |||
days.forEach(function(day, idx) { | |||
var val = daily[day] || 0; | |||
var h = Math.max(3, Math.round((val/max)*70)); | |||
var isToday = idx === 29; | |||
var col = isToday ? '#ff6600' : (val > 0 ? '#cc5500' : '#222'); | |||
bHtml += '<div title="' + day + ': ' + val + ' edits" style="flex:1;background:' + col + ';height:' + h + 'px;border-radius:2px 2px 0 0;min-width:4px;"></div>'; | |||
lHtml += '<div style="flex:1;text-align:center;white-space:nowrap;">' + (idx%5===0||isToday ? day.slice(5) : '') + '</div>'; | |||
}); | |||
wrap.innerHTML = bHtml; labelWrap.innerHTML = lHtml; | |||
} | |||
function renderTable(rows, total, period) { | |||
var tbody = document.getElementById('geo-table-body'); | |||
var titleEl = document.getElementById('geo-table-title'); | |||
if (!tbody) return; | |||
var labels = {day:'Today', week:'This Week', month:'This Month', all:'All Time'}; | |||
if (titleEl) titleEl.textContent = 'Users by Country – ' + (labels[period]||'All Time'); | |||
if (!rows.length) { | |||
tbody.innerHTML = '<tr><td colspan="5" style="padding:20px;text-align:center;color:#555;">No activity for this period.</td></tr>'; | |||
return; | |||
} | |||
var html = ''; | |||
rows.forEach(function(row, i) { | |||
var bar = '<div style="display:inline-block;background:#ff6600;height:8px;border-radius:2px;width:' + Math.round(row.pct) + '%;min-width:2px;vertical-align:middle;"></div><span style="color:#666;font-size:0.8em;margin-left:6px;">' + row.pct.toFixed(1) + '%</span>'; | |||
html += '<tr style="background:' + (i%2===0?'#1a1a1a':'#1e1e1e') + ';border-top:1px solid #222;">' + | |||
'<td style="padding:10px 16px;color:#666;">' + (i+1) + '</td>' + | |||
'<td style="padding:10px 16px;">' + (COUNTRY_MAP[row.lang]||('🌐 '+row.lang)) + '</td>' + | |||
'<td style="padding:10px 16px;text-align:right;font-weight:bold;color:#ff6600;">' + row.users + '</td>' + | |||
'<td style="padding:10px 16px;">' + bar + '</td>' + | |||
'<td style="padding:10px 16px;text-align:right;color:#aaa;">' + row.edits + '</td></tr>'; | |||
}); | |||
tbody.innerHTML = html; | |||
} | |||
var cachedUsers = null, cachedChanges = null, cachedBabel = {}, currentPeriod = 'day'; | |||
function showPeriod(period) { | |||
currentPeriod = period; | |||
['day','week','month','all'].forEach(function(p) { | |||
var btn = document.getElementById('geo-btn-'+p); | |||
if (!btn) return; | |||
if (p === period) { | |||
btn.style.background='#ff6600'; btn.style.color='#fff'; | |||
btn.style.borderColor='#ff6600'; btn.style.fontWeight='bold'; | |||
} else { | |||
btn.style.background='#333'; btn.style.color='#eee'; | |||
btn.style.borderColor='#555'; btn.style.fontWeight='normal'; | |||
} | |||
}); | |||
if (!cachedChanges || !cachedUsers) return; | |||
var cutoff = period === 'all' ? '1970-01-01' : startOf(period).toISOString(); | |||
var userEdits = {}; | |||
cachedChanges.forEach(function(c) { | |||
if (c.timestamp >= cutoff) userEdits[c.user] = (userEdits[c.user]||0)+1; | |||
}); | |||
var langUsers = {}, langEdits = {}; | |||
cachedUsers.forEach(function(u) { | |||
var langs = cachedBabel[u.name] || []; | |||
if (!langs.length) langs = ['unknown']; | |||
var edits = userEdits[u.name] || 0; | |||
var lang = langs[0]; | |||
langUsers[lang] = (langUsers[lang]||0)+1; | |||
langEdits[lang] = (langEdits[lang]||0)+edits; | |||
}); | |||
var total = cachedUsers.length; | |||
var cEl = document.getElementById('geo-countries'); | |||
if (cEl) cEl.textContent = Object.keys(langUsers).filter(function(l){return l!=='unknown';}).length; | |||
var rows = Object.keys(langUsers).map(function(lang) { | |||
return {lang:lang, users:langUsers[lang], edits:langEdits[lang]||0, pct:total>0?(langUsers[lang]/total*100):0}; | |||
}).sort(function(a,b) { | |||
if (a.lang==='unknown') return 1; if (b.lang==='unknown') return -1; return b.users-a.users; | |||
}); | |||
renderTable(rows, total, period); | |||
drawMap(rows.filter(function(r){return r.lang!=='unknown';}).map(function(r){return r.lang;})); | |||
} | |||
window.geoShowPeriod = showPeriod; | |||
function boot() { | |||
var el = document.getElementById('geo-dashboard'); | |||
if (!el) return; | |||
Promise.all([fetchUsers(), fetchChanges()]).then(function(results) { | |||
cachedUsers = results[0]; cachedChanges = results[1]; | |||
var now = new Date(); | |||
var tEl = document.getElementById('geo-total'); | |||
if (tEl) tEl.textContent = cachedUsers.length; | |||
var updEl = document.getElementById('geo-updated'); | |||
if (updEl) updEl.textContent = 'Updated: ' + now.toLocaleTimeString(); | |||
var dStart = startOf('day').toISOString(); | |||
var wStart = startOf('week').toISOString(); | |||
var mStart = startOf('month').toISOString(); | |||
var aDay=new Set(), aWeek=new Set(), aMonth=new Set(); | |||
var daily = {}; | |||
cachedChanges.forEach(function(c) { | |||
if (c.timestamp >= dStart) aDay.add(c.user); | |||
if (c.timestamp >= wStart) aWeek.add(c.user); | |||
if (c.timestamp >= mStart) aMonth.add(c.user); | |||
var day = c.timestamp.slice(0,10); | |||
daily[day] = (daily[day]||0)+1; | |||
}); | |||
var adEl = document.getElementById('geo-active-day'); | |||
var awEl = document.getElementById('geo-active-week'); | |||
var amEl = document.getElementById('geo-active-month'); | |||
if (adEl) adEl.textContent = aDay.size; | |||
if (awEl) awEl.textContent = aWeek.size; | |||
if (amEl) amEl.textContent = aMonth.size; | |||
renderActivityBars(daily); | |||
var babelPromises = cachedUsers.map(function(u) { | |||
return fetchBabel(u.name).then(function(langs) { cachedBabel[u.name] = langs; }); | |||
}); | |||
Promise.all(babelPromises).then(function() { showPeriod('day'); }); | |||
// wire up buttons | |||
['day','week','month','all'].forEach(function(p) { | |||
var btn = document.getElementById('geo-btn-'+p); | |||
if (btn) btn.addEventListener('click', function() { showPeriod(p); }); | |||
}); | |||
}).catch(function(e) { console.error('GEO dashboard error', e); }); | |||
} | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', boot); | |||
} else { | |||
setTimeout(boot, 100); | |||
} | |||
})(); | })(); | ||