MediaWiki:Common.js: Difference between revisions

Add Users by Country counter widget for Admin page
Add geo dashboard script: Users by Country with day/week/month periods, activity chart, world map
 
(5 intermediate revisions by the same user not shown)
Line 696: Line 696:
(function() {
(function() {
   'use strict';
   'use strict';
  // Only run on the User:Admin page
   if (mw.config.get('wgPageName') !== 'User:Admin') return;
   if (mw.config.get('wgPageName') !== 'User:Admin') return;


  // Language code to country/region mapping
   var langToCountry = {
   var langToCountry = {
     'en': '🇺🇸 English-speaking',
     'en': '🇺🇸 English-speaking', 'de': '🇩🇪 Germany / German-speaking',
    'de': '🇩🇪 Germany / German-speaking',
     'fr': '🇫🇷 France / French-speaking', 'es': '🇪🇸 Spain / Spanish-speaking',
     'fr': '🇫🇷 France / French-speaking',
     'pt': '🇵🇹 Portugal / Portuguese-speaking', 'it': '🇮🇹 Italy / Italian-speaking',
    'es': '🇪🇸 Spain / Spanish-speaking',
     'nl': '🇳🇱 Netherlands / Dutch-speaking', 'pl': '🇵🇱 Poland / Polish-speaking',
     'pt': '🇵🇹 Portugal / Portuguese-speaking',
     'ru': '🇷🇺 Russia / Russian-speaking', 'ja': '🇯🇵 Japan',
    'it': '🇮🇹 Italy / Italian-speaking',
     'zh': '🇨🇳 China / Chinese-speaking', 'ar': '🇸🇦 Arabic-speaking',
     'nl': '🇳🇱 Netherlands / Dutch-speaking',
     'tr': '🇹🇷 Turkey / Turkish-speaking', 'sv': '🇸🇪 Sweden / Swedish-speaking',
    'pl': '🇵🇱 Poland / Polish-speaking',
     'da': '🇩🇰 Denmark / Danish-speaking', 'fi': '🇫🇮 Finland / Finnish-speaking',
     'ru': '🇷🇺 Russia / Russian-speaking',
     'nb': '🇳🇴 Norway / Norwegian-speaking', 'cs': '🇨🇿 Czech Republic',
    'ja': '🇯🇵 Japan',
     'hu': '🇭🇺 Hungary', 'ro': '🇷🇴 Romania', 'el': '🇬🇷 Greece / Greek-speaking',
     'zh': '🇨🇳 China / Chinese-speaking',
     'ko': '🇰🇷 Korea / Korean-speaking', 'uk': '🇺🇦 Ukraine / Ukrainian-speaking',
    'ar': '🇸🇦 Arabic-speaking',
     'he': '🇮🇱 Israel / Hebrew-speaking', 'id': '🇮🇩 Indonesia',
     'tr': '🇹🇷 Turkey / Turkish-speaking',
     'vi': '🇻🇳 Vietnam', 'th': '🇹🇭 Thailand', 'hi': '🇮🇳 India / Hindi-speaking',
    'sv': '🇸🇪 Sweden / Swedish-speaking',
     'bn': '🇧🇩 Bangladesh / Bengali-speaking', 'fa': '🇮🇷 Iran / Persian-speaking'
     'da': '🇩🇰 Denmark / Danish-speaking',
    'fi': '🇫🇮 Finland / Finnish-speaking',
     'nb': '🇳🇴 Norway / Norwegian-speaking',
    'cs': '🇨🇿 Czech Republic',
     'hu': '🇭🇺 Hungary',
    'ro': '🇷🇴 Romania',
    'el': '🇬🇷 Greece / Greek-speaking',
     'ko': '🇰🇷 Korea / Korean-speaking',
    'uk': '🇺🇦 Ukraine / Ukrainian-speaking',
     'he': '🇮🇱 Israel / Hebrew-speaking',
    'id': '🇮🇩 Indonesia',
     'vi': '🇻🇳 Vietnam',
    'th': '🇹🇭 Thailand',
    'hi': '🇮🇳 India / Hindi-speaking',
     'bn': '🇧🇩 Bangladesh / Bengali-speaking',
    'fa': '🇮🇷 Iran / Persian-speaking'
   };
   };
  function renderResults(langCounts, totalUsers) {
    var container = document.getElementById('country-user-counter');
    if (!container) return;
    var sorted = Object.keys(langCounts)
      .filter(function(l) { return langCounts[l] > 0; })
      .sort(function(a, b) { return langCounts[b] - langCounts[a]; });
    var html = '<style>';
    html += '#cuc-wrap{font-family:sans-serif}';
    html += '#cuc-stats{display:flex;gap:20px;margin-bottom:14px;flex-wrap:wrap}';
    html += '.cuc-stat-card{background:#222;border:1px solid #444;border-radius:6px;padding:10px 18px;min-width:120px;text-align:center}';
    html += '.cuc-stat-num{font-size:1.8em;font-weight:bold;color:#e07000}';
    html += '.cuc-stat-lbl{font-size:0.8em;color:#aaa;margin-top:2px}';
    html += '#cuc-table{width:100%;border-collapse:collapse;margin-top:4px}';
    html += '#cuc-table th{background:#2a2a2a;color:#e07000;padding:8px 12px;text-align:left;border:1px solid #444;font-size:0.9em}';
    html += '#cuc-table td{padding:7px 12px;border:1px solid #2e2e2e;color:#ddd;font-size:0.9em}';
    html += '#cuc-table tr:nth-child(even) td{background:#1c1c1c}';
    html += '#cuc-table tr:hover td{background:#252525}';
    html += '.cuc-bar-wrap{background:#111;border-radius:3px;height:14px;overflow:hidden}';
    html += '.cuc-bar{height:14px;background:linear-gradient(90deg,#e07000,#ff9a00);border-radius:3px;transition:width 0.4s ease}';
    html += '.cuc-no-data{background:#1a1a1a;border:1px dashed #444;border-radius:6px;padding:16px;color:#aaa;line-height:1.6}';
    html += '.cuc-no-data code{background:#222;padding:2px 6px;border-radius:3px;color:#e07000;font-size:0.9em}';
    html += '#cuc-timestamp{color:#555;font-size:0.8em;margin-top:10px}';
    html += '</style>';
    html += '<div id="cuc-wrap">';
    // Stats cards
    html += '<div id="cuc-stats">';
    html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + totalUsers + '</div><div class="cuc-stat-lbl">Total Users</div></div>';
    html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + sorted.length + '</div><div class="cuc-stat-lbl">Countries / Regions</div></div>';
    var locatedUsers = sorted.reduce(function(sum, l) { return sum + langCounts[l]; }, 0);
    html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + locatedUsers + '</div><div class="cuc-stat-lbl">Users with Location</div></div>';
    html += '</div>';
    if (sorted.length === 0) {
      html += '<div class="cuc-no-data">';
      html += '<strong>📍 No country data available yet.</strong><br>';
      html += 'This counter tracks users by country using the <strong>Babel extension</strong> language categories.<br>';
      html += 'Users can declare their language/country by adding babel tags to their user page:<br><br>';
      html += '<code>{{#babel:en|de|fr}}</code><br><br>';
      html += 'Once users add babel tags, this counter will automatically display their country distribution.<br>';
      html += 'Currently tracking <strong>' + totalUsers + ' registered users</strong> on this wiki.';
      html += '</div>';
    } else {
      var maxCount = langCounts[sorted[0]] || 1;
      html += '<table id="cuc-table"><thead><tr>';
      html += '<th style="width:40px">#</th>';
      html += '<th>Country / Region</th>';
      html += '<th style="width:70px;text-align:center">Users</th>';
      html += '<th style="width:200px">Distribution</th>';
      html += '</tr></thead><tbody>';
      sorted.forEach(function(lang, idx) {
        var count = langCounts[lang];
        var label = langToCountry[lang] || ('🌐 ' + lang.toUpperCase());
        var pct = Math.round((count / locatedUsers) * 100);
        var barWidth = Math.round((count / maxCount) * 180);
        html += '<tr>';
        html += '<td style="color:#666;text-align:center">' + (idx + 1) + '</td>';
        html += '<td>' + label + '</td>';
        html += '<td style="text-align:center;font-weight:bold;color:#e07000">' + count + '</td>';
        html += '<td><div class="cuc-bar-wrap"><div class="cuc-bar" style="width:' + barWidth + 'px"></div></div></td>';
        html += '</tr>';
      });
      html += '</tbody></table>';
    }
    html += '<div id="cuc-timestamp">Last updated: ' + new Date().toLocaleString() + '</div>';
    html += '</div>';
    container.innerHTML = html;
  }


   function buildCounter() {
   function buildCounter() {
     var container = document.getElementById('country-user-counter');
     var container = document.getElementById('country-user-counter');
     if (!container) return;
     if (!container) return;
    container.innerHTML = '<p style="color:#aaa;font-style:italic">⏳ Loading user statistics by country...</p>';


    container.innerHTML = '<p style="color:#aaa; font-style:italic;">⏳ Fetching user data...</p>';
    // Step 1: Get all users
     var allUsers = [];
     var allUsers = [];
    var langCounts = {};
    var langs = Object.keys(langToCountry);


     function fetchUsers(aufrom) {
     function fetchUsers(aufrom) {
       var params = {
       var params = { action: 'query', list: 'allusers', aulimit: 500, format: 'json' };
        action: 'query',
        list: 'allusers',
        aulimit: 500,
        format: 'json'
      };
       if (aufrom) params.aufrom = aufrom;
       if (aufrom) params.aufrom = aufrom;
       return $.getJSON(mw.util.wikiScript('api'), params).then(function(data) {
       return $.getJSON(mw.util.wikiScript('api'), params).then(function(data) {
         var users = (data.query && data.query.allusers) || [];
         var users = (data.query && data.query.allusers) || [];
         allUsers = allUsers.concat(users);
         allUsers = allUsers.concat(users);
         if (data.continue && data.continue.aufrom) {
         if (data['continue'] && data['continue'].aufrom) return fetchUsers(data['continue'].aufrom);
          return fetchUsers(data.continue.aufrom);
        }
       });
       });
     }
     }


     // Step 2: For each user, get their language preference
     function fetchLangCat(lang) {
    function fetchUserLanguages() {
      return $.getJSON(mw.util.wikiScript('api'), {
      var langCounts = {};
        action: 'query', list: 'categorymembers',
       var totalFetched = 0;
        cmtitle: 'Category:User_' + lang,
       var batchSize = 50;
        cmtype: 'page', cmnamespace: 2, cmlimit: 500, format: 'json'
       var userNames = allUsers.map(function(u) { return u.name; });
      }).then(function(data) {
      var batches = [];
        var m = (data.query && data.query.categorymembers) || [];
        if (m.length > 0) langCounts[lang] = (langCounts[lang] || 0) + m.length;
      }).catch(function() {});
    }
 
    fetchUsers().then(function() {
       var d = $.Deferred().resolve().promise();
       langs.forEach(function(lang) { d = d.then(function() { return fetchLangCat(lang); }); });
       return d;
    }).then(function() {
      renderResults(langCounts, allUsers.length);
    }).catch(function(e) {
      container.innerHTML = '<p style="color:#f66">⚠️ Error loading data: ' + String(e) + '</p>';
    });
  }
 
  $(document).ready(function() { buildCounter(); });
})();
// =====================================================
// End: Users by Country Counter
// =====================================================
 
 
// ===== Article Overview Page - Category/Subcategory/Title/Words/Focus Table =====
(function() {
  if (mw.config.get('wgPageName') !== 'Article_Overview') return;
 
  var apiBase = mw.util.wikiScript('api');
  var allRows = [];
  var tableBody = null;
  var statusSpan = null;
 
  $(document).ready(function() {
    var wrapper = document.getElementById('article-overview-wrapper');
    if (!wrapper) return;
 
    // Build controls row with button + status
    var controls = document.createElement('div');
    controls.style.marginBottom = '14px';
 
    var btn = document.createElement('button');
    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.addEventListener('click', downloadCSV);
    controls.appendChild(btn);
 
    statusSpan = document.createElement('span');
    statusSpan.style.cssText = 'margin-left:14px;font-size:13px;color:#aaa;vertical-align:middle;';
    statusSpan.textContent = 'Loading…';
    controls.appendChild(statusSpan);
    wrapper.appendChild(controls);
 
    // Build table
    var table = document.createElement('table');
    table.style.cssText = 'width:100%;border-collapse:collapse;font-size:13px;';


      for (var i = 0; i < userNames.length; i += batchSize) {
    var thead = document.createElement('thead');
        batches.push(userNames.slice(i, i + batchSize));
    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();
  });
 
  function getCategoryMembers(catName, cmtype, continueParam, accumulated, callback) {
    var params = {
      action: 'query', list: 'categorymembers',
      cmtitle: 'Category:' + catName,
      cmlimit: 500, cmtype: cmtype, format: 'json'
    };
    if (continueParam) params.cmcontinue = continueParam;
    $.getJSON(apiBase, params).done(function(data) {
      var members = (data.query && data.query.categorymembers) ? data.query.categorymembers : [];
      members.forEach(function(m){ accumulated.push(m); });
      if (data.continue && data.continue.cmcontinue) {
        getCategoryMembers(catName, cmtype, data.continue.cmcontinue, accumulated, callback);
      } else {
        callback(accumulated);
       }
       }
    }).fail(function(){ callback(accumulated); });
  }
  function escHtml(str) {
    return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  }
  function renderTable(rows) {
    if (!tableBody) return;
    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;
    }
    var html = '';
    rows.forEach(function(row, i) {
      var bg = i % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.07)';
      html += '<tr style="background:' + bg + ';">';
      html += '<td style="padding:6px 10px;border:1px solid #444;">' + escHtml(row.category) + '</td>';
      html += '<td style="padding:6px 10px;border:1px solid #444;">' + escHtml(row.sub) + '</td>';
      html += '<td style="padding:6px 10px;border:1px solid #444;"><a href="' + mw.util.getUrl(row.title) + '">' + escHtml(row.title) + '</a></td>';
      html += '<td style="padding:6px 10px;border:1px solid #444;text-align:right;">' + escHtml(String(row.words)) + '</td>';
      html += '<td style="padding:6px 10px;border:1px solid #444;">' + escHtml(row.focus) + '</td>';
      html += '</tr>';
    });
    tableBody.innerHTML = html;
  }
  function buildTable() {
    if (statusSpan) statusSpan.textContent = 'Fetching categories…';
    $.getJSON(apiBase, { action:'query', list:'allcategories', aclimit:500, format:'json' })
      .done(function(data) {
        var topCats = (data.query && data.query.allcategories) ? data.query.allcategories.map(function(c){ return c['*']; }) : [];
        var rows = [];
        var totalCats = topCats.length;
        var processed = 0;


      function processBatch(idx) {
        if (totalCats === 0) {
        if (idx >= batches.length) {
          if (tableBody) tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#aaa;">No categories found.</td></tr>';
           renderResults(langCounts, allUsers.length);
           if (statusSpan) statusSpan.textContent = '';
           return;
           return;
         }
         }
         var batch = batches[idx];
 
        return $.getJSON(mw.util.wikiScript('api'), {
         function checkDone() {
           action: 'query',
          processed++;
          list: 'users',
          if (statusSpan) statusSpan.textContent = 'Loading… (' + processed + '/' + totalCats + ' categories)';
          ususers: batch.join('|'),
          if (processed === totalCats) {
          usprop: 'groups',
            rows.sort(function(a,b){
          format: 'json'
              var ca = a.category.toLowerCase(), cb = b.category.toLowerCase();
        }).then(function() {
              if (ca < cb) return -1; if (ca > cb) return 1;
          // Language isn't directly available via users list
              var sa = a.sub.toLowerCase(), sb = b.sub.toLowerCase();
          // Use userinfo for current user only, so we fallback to babel categories
              if (sa < sb) return -1; if (sa > sb) return 1;
           processBatch(idx + 1);
              return a.title.localeCompare(b.title);
            });
            allRows = rows;
            renderTable(rows);
            if (statusSpan) statusSpan.textContent = rows.length + ' article entries loaded.';
          }
        }
 
        topCats.forEach(function(catName) {
           getCategoryMembers(catName, 'page|subcat', null, [], function(members) {
            var pages = members.filter(function(m){ return m.ns === 0; });
            var subcats = members.filter(function(m){ return m.ns === 14; });
 
            if (pages.length === 0 && subcats.length === 0) {
              checkDone(); return;
            }
 
            var subTotal = pages.length + subcats.length;
            var subProcessed = 0;
 
            function subDone() {
              subProcessed++;
              if (subProcessed >= subTotal) checkDone();
            }
 
            pages.forEach(function(page) {
              rows.push({ category: catName, sub: '', title: page.title, words: 'n/a', focus: catName });
              subDone();
            });
 
            subcats.forEach(function(subcat) {
              var subName = subcat.title.replace('Category:','');
              getCategoryMembers(subName, 'page', null, [], function(subPages) {
                subPages.filter(function(m){ return m.ns === 0; }).forEach(function(page) {
                  rows.push({ category: catName, sub: subName, title: page.title, words: 'n/a', focus: catName });
                });
                subDone();
              });
            });
           });
         });
         });
       }
       })
       processBatch(0);
       .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>';
      });
  }
 
  function downloadCSV() {
    if (!allRows || allRows.length === 0) {
      alert('No data to download yet. Please wait for the table to finish loading.');
      return;
     }
     }
    var csv = 'Category,Subcategory,Title,Words,Focus\n';
    allRows.forEach(function(row) {
      csv += ['category','sub','title','words','focus'].map(function(k){
        return '"' + String(row[k]).replace(/"/g,'""') + '"';
      }).join(',') + '\n';
    });
    var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    var url = URL.createObjectURL(blob);
    var a = document.createElement('a');
    a.href = url; a.download = 'article-overview.csv';
    document.body.appendChild(a); a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }
})();
// ═══════════════════════════════════════════════════════════
// 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);
  }


     // Step 3: Get language data from Babel extension categories
  var COUNTRY_MAP = {
     function fetchBabelData() {
     'en':'🇺🇸 English / US','de':'🇩🇪 German','fr':'🇫🇷 French','es':'🇪🇸 Spanish',
      var langCounts = {};
    'it':'🇮🇹 Italian','pt':'🇧🇷 Portuguese','ru':'🇷🇺 Russian','ja':'🇯🇵 Japanese',
      var promises = [];
    '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'
  };


       // Fetch members of language categories (e.g., Category:User_en, Category:User_de, etc.)
  function fetchUsers() {
       var langs = Object.keys(langToCountry);
    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 fetchLangCategory(lang, offset) {
  function fetchChanges() {
        return $.getJSON(mw.util.wikiScript('api'), {
    return fetch(API + '?action=query&list=recentchanges&rclimit=500&rcprop=user%7Ctimestamp&format=json')
          action: 'query',
      .then(function(r) { return r.json(); })
          list: 'categorymembers',
      .then(function(d) { return d.query.recentchanges || []; });
          cmtitle: 'Category:User_' + lang,
  }
          cmtype: 'page',
          cmnamespace: 2,
          cmlimit: 500,
          cmcontinue: offset || undefined,
          format: 'json'
        }).then(function(data) {
          var members = (data.query && data.query.categorymembers) || [];
          if (!langCounts[lang]) langCounts[lang] = 0;
          langCounts[lang] += members.length;
          if (data.continue && data.continue.cmcontinue) {
            return fetchLangCategory(lang, data.continue.cmcontinue);
          }
        }).catch(function() {
          // Category may not exist, ignore
        });
      }


       var chain = $.Deferred().resolve();
  function fetchBabel(username) {
       langs.forEach(function(lang) {
    return fetch(API + '?action=query&titles=User:' + encodeURIComponent(username) + '&prop=categories&format=json')
         chain = chain.then(function() {
       .then(function(r) { return r.json(); })
           return fetchLangCategory(lang);
       .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;
       });
       });
  }


      return chain.then(function() {
  function drawMap(activeLangs) {
        return langCounts;
    var svg = document.getElementById('geo-world-map');
      });
    if (!svg) return;
     }
     svg.innerHTML = '';


     // Render the results table
     var bg = document.createElementNS('http://www.w3.org/2000/svg','rect');
     function renderResults(langCounts, totalUsers) {
     bg.setAttribute('width','900'); bg.setAttribute('height','440'); bg.setAttribute('fill','#0d1f33');
      var sorted = Object.keys(langCounts)
    svg.appendChild(bg);
        .filter(function(l) { return langCounts[l] > 0; })
        .sort(function(a, b) { return langCounts[b] - langCounts[a]; });


      var html = '<style>';
    var continents = [
       html += '#country-counter-table { width:100%; border-collapse:collapse; margin-top:10px; }';
      'M80,100 L240,95 L255,200 L220,230 L170,235 L110,220 L75,170 Z',
       html += '#country-counter-table th { background:#2a2a2a; color:#e07000; padding:8px 12px; text-align:left; border:1px solid #444; }';
      'M155,240 L220,235 L235,360 L195,385 L155,370 L138,320 Z',
       html += '#country-counter-table td { padding:7px 12px; border:1px solid #333; color:#ddd; }';
      'M385,90 L480,85 L490,160 L450,175 L400,170 L378,140 Z',
       html += '#country-counter-table tr:nth-child(even) td { background:#1a1a1a; }';
       'M390,175 L460,170 L470,320 L430,345 L388,335 L375,280 Z',
       html += '#country-counter-table tr:hover td { background:#252525; }';
       'M490,80 L750,75 L760,230 L700,250 L520,240 L485,180 Z',
       html += '.bar-cell { width:40%; }';
       'M640,280 L740,275 L748,355 L700,368 L638,355 Z',
       html += '.bar { height:14px; background:#e07000; border-radius:3px; display:inline-block; min-width:2px; }';
       'M230,50 L290,45 L295,90 L255,95 L228,82 Z'
       html += '</style>';
    ];
    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);
    });


       html += '<p style="color:#ccc; margin-bottom:8px;">📊 <strong>Total registered users:</strong> ' + totalUsers + '</p>';
    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);
    });


      if (sorted.length === 0) {
    var labels = [{x:160,y:175,t:'North America'},{x:450,y:130,t:'Europe'},{x:620,y:165,t:'Asia'},
        html += '<p style="color:#aaa;">No Babel language categories found. Users can add their language to their profile using <code>{{#babel:en}}</code> on their user page.</p>';
                  {x:430,y:265,t:'Africa'},{x:192,y:305,t:'South America'},{x:693,y:325,t:'Oceania'}];
        html += '<p style="color:#888; font-size:0.9em;">💡 Tip: To enable country tracking, ask users to add babel tags to their user pages.</p>';
    labels.forEach(function(loc) {
      } else {
      var text = document.createElementNS('http://www.w3.org/2000/svg','text');
        var maxCount = langCounts[sorted[0]] || 1;
      text.setAttribute('x',loc.x); text.setAttribute('y',loc.y);
        html += '<table id="country-counter-table"><thead><tr><th>#</th><th>Country / Region</th><th>Users</th><th class="bar-cell">Distribution</th></tr></thead><tbody>';
      text.setAttribute('fill','#444'); text.setAttribute('font-size','10');
        sorted.forEach(function(lang, idx) {
      text.setAttribute('font-family','sans-serif'); text.setAttribute('text-anchor','middle');
          var count = langCounts[lang];
      text.textContent = loc.t; svg.appendChild(text);
          var label = langToCountry[lang] || lang.toUpperCase();
    });
          var barWidth = Math.round((count / maxCount) * 200);
  }
          html += '<tr>';
          html += '<td>' + (idx + 1) + '</td>';
          html += '<td>' + label + '</td>';
          html += '<td style="text-align:center; font-weight:bold;">' + count + '</td>';
          html += '<td class="bar-cell"><span class="bar" style="width:' + barWidth + 'px"></span></td>';
          html += '</tr>';
        });
        html += '</tbody></table>';
      }


       html += '<p style="color:#666; font-size:0.85em; margin-top:10px;">ℹ️ Based on Babel language categories. Last loaded: ' + new Date().toLocaleString() + '</p>';
  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;
  }


       container.innerHTML = html;
  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';


     // Main flow: fetch users, then babel categories
  function showPeriod(period) {
     fetchUsers().then(function() {
     currentPeriod = period;
       return fetchBabelData();
     ['day','week','month','all'].forEach(function(p) {
     }).then(function(langCounts) {
      var btn = document.getElementById('geo-btn-'+p);
       renderResults(langCounts, allUsers.length);
      if (!btn) return;
     }).catch(function(err) {
      if (p === period) {
       container.innerHTML = '<p style="color:#f66;">Error loading data: ' + (err && err.message ? err.message : String(err)) + '</p>';
        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;}));
   }
   }


   // Run after DOM is ready
   window.geoShowPeriod = showPeriod;
  $(document).ready(function() {
 
     buildCounter();
  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);
  }
})();
})();
// =====================================================
// End: Users by Country Counter
// =====================================================