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;


  function getTableBody() {
    var table = document.getElementById('article-overview-table');
    if (!table) return null;
    var tb = table.querySelector('tbody');
    return tb;
  }
  // Inject the Download button and status span above the table
   $(document).ready(function() {
   $(document).ready(function() {
     var wrapper = document.getElementById('article-overview-wrapper');
     var wrapper = document.getElementById('article-overview-wrapper');
    var table = document.getElementById('article-overview-table');
     if (!wrapper) return;
     if (!wrapper || !table) return;


     // Create or find controls div
     // Build controls row with button + status
     var controls = document.getElementById('article-overview-controls');
     var controls = document.createElement('div');
    if (!controls) {
     controls.style.marginBottom = '14px';
      controls = document.createElement('div');
      controls.id = 'article-overview-controls';
      controls.style.marginBottom = '14px';
      wrapper.insertBefore(controls, table);
     } else {
      controls.style.marginBottom = '14px';
    }


    // Create Download button
     var btn = document.createElement('button');
     var btn = document.createElement('button');
    btn.id = 'download-csv-btn';
     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);


     // Create status span
     statusSpan = document.createElement('span');
    var statusSpan = document.createElement('span');
    statusSpan.id = 'overview-status';
     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) {
    var tbody = getTableBody();
     if (!tableBody) return;
     if (!tbody) return;
     if (!rows || rows.length === 0) {
     if (!rows || rows.length === 0) {
       tbody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#aaa;">No articles found.</td></tr>';
       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>';
     });
     });
     tbody.innerHTML = html;
     tableBody.innerHTML = html;
   }
   }


   function buildTable() {
   function buildTable() {
    var status = document.getElementById('overview-status');
     if (statusSpan) statusSpan.textContent = 'Fetching categories…';
     if (status) status.textContent = 'Fetching categories…';


     $.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) {
          var tbody = getTableBody();
           if (tableBody) tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#aaa;">No categories found.</td></tr>';
           if (tbody) tbody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#aaa;">No categories found.</td></tr>';
           if (statusSpan) statusSpan.textContent = '';
           if (status) status.textContent = '';
           return;
           return;
         }
         }
Line 948: Line 951:
         function checkDone() {
         function checkDone() {
           processed++;
           processed++;
           if (status) status.textContent = 'Loading… (' + processed + '/' + totalCats + ' categories processed)';
           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 (status) status.textContent = rows.length + ' article entries loaded.';
             if (statusSpan) statusSpan.textContent = rows.length + ' article entries loaded.';
           }
           }
         }
         }
Line 998: Line 1,001:
       })
       })
       .fail(function() {
       .fail(function() {
        var tbody = getTableBody();
         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 (tbody) tbody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#f55;">Error loading data. Check API access.</td></tr>';
       });
       });
   }
   }
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);
  }
})();
})();