MediaWiki:Common.js: Unterschied zwischen den Versionen

Keine Bearbeitungszusammenfassung
Markierung: Zurückgesetzt
Keine Bearbeitungszusammenfassung
Markierung: Manuelle Zurücksetzung
Zeile 740: Zeile 740:
// -------------------------------------
// -------------------------------------


/* === ADOS Cargo → Chart.js (Callback-Version) ============================== */
/* === ADOS Multi-Serien-Chart (Chart.js) ============================= *
/* Nutzung im Wikitext:
* Lädt Chart.js sicher (asynchron) und baut Diagramme aus Cargo-Tabellen.
  <div class="ados-chart-multi" data-type="line|bar" data-title="…" data-hide-table="true"></div>
* Benötigt im Artikel: <div class="ados-chart-multi"> + Cargo-Query als |format=table
  {{#cargo_query:
* ==================================================================== */
    tables=Abfuellungen
    |fields=Erscheinungsjahr=Jahr, Serie, COUNT(*)=Anzahl  <!-- oder SUM(Anzahl)=Anzahl -->
    |group by=Jahr, Serie
    |order by=Jahr, Serie
    |format=table
  }}
*/


(function () {
(function () {
  'use strict';
   // 1) Chart.js nur 1x laden und erst dann rendern
 
  var _chartReady = null;
   // ---- Chart.js laden (Callback-Stil) -------------------------------------
   function ensureChartJS() {
   function ensureChartJS(cb) {
     if (_chartReady) return _chartReady;
     if (window.Chart) return cb();
    _chartReady = new Promise(function (resolve, reject) {
    var s = document.createElement('script');
      if (window.Chart) return resolve();
    s.src = 'https://cdn.jsdelivr.net/npm/chart.js';
      var s = document.createElement('script');
    s.async = true;
      s.src = 'https://cdn.jsdelivr.net/npm/chart.js'; // UMD-Bundle
    s.onload = cb;
      s.async = true;
    document.head.appendChild(s);
      s.onload = function(){ resolve(); };
      s.onerror = function(){ console.error('Chart.js konnte nicht geladen werden'); reject(); };
      document.head.appendChild(s);
    });
    return _chartReady;
   }
   }


   // ---- Farben (gut unterscheidbar) ----------------------------------------
   // 2) Farben je Serie (gut unterscheidbar)
   const ADOS_COLORS = {
   var ADOS_COLORS = {
     'A Dream of Scotland':               '#C2410C', // Kupfer
     'A Dream of Scotland':                 '#C2410C', // Kupferbraun
     'A Dream of Ireland':               '#15803D', // Grün
     'A Dream of Ireland':                 '#15803D', // Flaschengrün
     'A Dream of... – Der Rest der Welt': '#1D4ED8', // Blau  (EN-Dash!)
     'A Dream of... – Der Rest der Welt':   '#1D4ED8', // Mittelblau
     'Friendly Mr. Z':                   '#9333EA', // Violett
     'Friendly Mr. Z':                     '#9333EA', // Violett
     'Die Whisky Elfen':                 '#0891B2', // Türkis
     'Die Whisky Elfen':                   '#0891B2', // Türkis
     'The Fine Art of Whisky':           '#CA8A04'  // Gold
     'The Fine Art of Whisky':             '#CA8A04'  // Goldgelb
   };
   };
   const COLOR_CYCLE = ['#2563EB','#16A34A','#F97316','#DC2626','#A855F7','#0EA5E9','#F59E0B','#10B981'];
   var COLOR_CYCLE = ['#2563eb','#16a34a','#f97316','#dc2626','#a855f7','#0ea5e9','#f59e0b','#10b981'];


  // ---- Helpers -------------------------------------------------------------
   function toYear(x){
   function toYear(x) {
     var n = parseInt(String(x).replace(/[^\d]/g,''),10);
     const n = parseInt(String(x).replace(/[^\d]/g, ''), 10);
     return isFinite(n) ? n : null;
     return isFinite(n) ? n : null;
   }
   }
   function getColor(name, used) {
   function getColor(name, used){
     if (ADOS_COLORS[name]) return ADOS_COLORS[name];
     if (ADOS_COLORS[name]) return ADOS_COLORS[name];
     const idx = used.size % COLOR_CYCLE.length;
     var i = used.size % COLOR_CYCLE.length;
     used.add(name);
     used.add(name);
     return COLOR_CYCLE[idx];
     return COLOR_CYCLE[i];
   }
   }


   // Tabelle -> {labels,datasets}
   // 3) Tabelle (Jahr | Serie | Anzahl) -> {labels, datasets}
   function buildDatasetsFromTable(tbl) {
   function buildDatasetsFromTable(tbl){
     const rows = Array.from(tbl.querySelectorAll('tr'));
     var rows = Array.from(tbl.querySelectorAll('tr'));
     if (rows.length < 2) return { labels: [], datasets: [] };
     if (rows.length < 2) return { labels:[], datasets:[] };


     const years = new Set();
     var yearsSet = new Set();
     const seriesMap = new Map(); // Serie -> Map(Jahr -> Wert)
     var bySeries  = new Map(); // serie -> Map(year -> count)


     rows.slice(1).forEach(tr => {
     rows.slice(1).forEach(function(tr){
       const tds = tr.querySelectorAll('td,th');
       var tds = tr.querySelectorAll('td,th');
       if (tds.length < 3) return;
       if (tds.length < 3) return;
       const y = toYear(tds[0].textContent);
       var y = toYear(tds[0].textContent.trim());
       const s = tds[1].textContent.trim();
       var s = tds[1].textContent.trim();
       const v = parseFloat(String(tds[2].textContent).replace(',', '.')) || 0;
       var v = parseFloat(tds[2].textContent.replace(',','.')) || 0;
       if (y == null) return;
       if (y == null) return;
       years.add(y);
       yearsSet.add(y);
       if (!seriesMap.has(s)) seriesMap.set(s, new Map());
       if (!bySeries.has(s)) bySeries.set(s, new Map());
       seriesMap.get(s).set(y, v);
       bySeries.get(s).set(y, v);
     });
     });


     const labels = Array.from(years).sort((a, b) => a - b).map(String);
     var years = Array.from(yearsSet).sort(function(a,b){return a-b;});
     const used = new Set();
     var used = new Set();
     const datasets = Array.from(seriesMap.entries()).map(([name, ym]) => {
     var labels = years.map(String);
       const c = getColor(name, used);
 
    var datasets = Array.from(bySeries.entries()).map(function(entry){
      var name = entry[0], yearMap = entry[1];
      var data = years.map(function(y){ return yearMap.get(y) || 0; });
       var color = getColor(name, used);
       return {
       return {
         label: name,
         label: name,
         data: labels.map(y => ym.get(+y) || 0),
         data: data,
         borderColor: c,
         borderColor: color,
         backgroundColor: c + '80',
         backgroundColor: color + '80',
         tension: 0.25,
         tension: 0.25,
         pointRadius: 3
         pointRadius: 3
Zeile 822: Zeile 822:
     });
     });


     return { labels, datasets };
     return { labels: labels, datasets: datasets };
   }
   }


   // ---- Zusatz: Gesamtzahl unter der Legende anzeigen ----------------------
   // 4) Einen Chart-Container rendern: nimmt die NÄCHSTE Tabelle als Datenquelle
function addTotalBelowLegend(chart) {
function renderOne(block){
   try {
   // schon gerendert? (z. B. durch AJAX/Minerva-Reloads)
    if (!chart || !chart.data || !chart.data.datasets || !chart.data.datasets.length) return;
  if (block.dataset.rendered === '1') return;


    // Werte addieren
  // nächste Tabelle (auch wenn sie in einem Wrapper steckt) finden
    const total = chart.data.datasets.reduce((sum, ds) => {
  var el = block.nextElementSibling, tbl = null, wrapToHide = null;
      const arr = Array.isArray(ds.data) ? ds.data : [];
  while (el) {
      return sum + arr.reduce((a, b) => a + (parseFloat(b) || 0), 0);
     if (/^H[1-6]$/.test(el.tagName) || (el.classList && el.classList.contains('ados-chart-multi'))) break;
    }, 0);
     if (el.tagName === 'TABLE') {
 
      tbl = el;
     // Elternkette: canvas -> wrap -> block(.ados-chart-multi)
     } else if (el.querySelector) {
    const wrap  = chart.canvas.parentNode;
       var t = el.querySelector('table');
    const block = wrap && wrap.parentNode ? wrap.parentNode : null;
       if (t) tbl = t;
    if (!block) return;
    }
 
    if (tbl) {
     // nur direktes Kind nehmen (verhindert Mehrfacheinträge)
       // Wrapper merken – aber später nur verstecken, wenn er "quasi nur" die Tabelle enthält
    let info = block.querySelector(':scope > .chart-total-info');
       wrapToHide = tbl.parentElement;
     if (!info) {
       break;
       info = document.createElement('div');
       info.className = 'chart-total-info';
       // Basisstil, falls kein CSS gesetzt ist
      info.style.textAlign = 'center';
      info.style.fontWeight = 'bold';
      info.style.fontSize = '1.05em';
      info.style.marginTop = '0.5rem';
       info.style.marginBottom = '0.35rem';
       info.style.color = '#444';
      block.appendChild(info);
     }
     }
     info.textContent = 'Gesamt: ' + total;
     el = el.nextElementSibling;
  } catch (e) {
    console.warn('[chart-total]', e);
   }
   }
}
   if (!tbl) return;
 
 
 
 
function isFlagTrue(v){
   if (v == null) return false;
  v = String(v).trim().toLowerCase();
  return v === 'true' || v === '1' || v === 'yes';
}
 
 
 
 
 
 
  // ---- Einzel-Chart rendern -----------------------------------------------
  function renderMultiChart(block) {
    if (block.dataset.rendered === '1') return;
 
    // nächste Tabelle nach dem Container suchen
    let tbl = block.nextElementSibling;
    while (tbl && tbl.tagName !== 'TABLE') tbl = tbl.nextElementSibling;
    if (!tbl) return;


    // Daten lesen
  var out = buildDatasetsFromTable(tbl);
    const out = buildDatasetsFromTable(tbl);
  if (!out.labels.length || !out.datasets.length) return;
    if (!out.labels.length || !out.datasets.length) return;


    // Tabelle ggf. ausblenden (data-hide-table="true")
  // Tabelle verstecken, wenn gewünscht (nur die Tabelle – oder den Wrapper, falls er sonst leer ist)
    const hide = (block.dataset.hideTable || '').toLowerCase() === 'true';
  var hide = (block.dataset.hideTable || '').toLowerCase() === 'true';
     if (hide) {
  if (hide) {
       tbl.setAttribute('aria-hidden', 'true');
    // Hat der Wrapper außer der Tabelle noch sichtbaren Inhalt?
    var onlyTable = wrapToHide && wrapToHide.children.length === 1 && wrapToHide.firstElementChild === tbl;
     if (onlyTable) {
      wrapToHide.setAttribute('aria-hidden','true');
      wrapToHide.style.display = 'none';
    } else {
       tbl.setAttribute('aria-hidden','true');
       tbl.style.display = 'none';
       tbl.style.display = 'none';
     }
     }
  }


    // Canvas-Wrapper (responsiv)
  // Zeichenfläche einsetzen (mobil/desktop)
    const wrap = document.createElement('div');
  var wrap = document.createElement('div');
    wrap.style.position = 'relative';
  wrap.style.position = 'relative';
    wrap.style.width = '100%';
  wrap.style.width = '100%';
    wrap.style.height = window.matchMedia('(min-width: 768px)').matches ? '450px' : '320px';
  wrap.style.height = block.dataset.height || (window.matchMedia('(min-width:768px)').matches ? '450px' : '300px');
  var canvas = document.createElement('canvas');
  wrap.appendChild(canvas);
  block.innerHTML = '';
  block.appendChild(wrap);


    const canvas = document.createElement('canvas');
  var type  = (block.dataset.type || 'line').toLowerCase();
    wrap.appendChild(canvas);
  var title = block.dataset.title || '';
    block.innerHTML = '';
  var cumulative = (block.dataset.cumulative || '').toLowerCase() === 'true';
    block.appendChild(wrap);


     const type  = (block.dataset.type  || 'line').toLowerCase(); // "line"|"bar"
  // optional: kumulative Werte pro Serie bauen
     const title = (block.dataset.title || '');
  if (cumulative) {
     out.datasets = out.datasets.map(function(ds){
      var acc = 0;
      return Object.assign({}, ds, {
        data: ds.data.map(function(v){ acc += v; return acc; })
      });
     });
  }


    // Chart.js laden + Diagramm zeichnen
  ensureChartJS().then(function(){
ensureChartJS(function(){
    new Chart(canvas.getContext('2d'), {
  const chart = new Chart(canvas.getContext('2d'), {
      type: type,
    type: type,
      data: { labels: out.labels, datasets: out.datasets },
    data: { labels: out.labels, datasets: out.datasets },
      options: {
    options: {
        responsive: true,
      responsive: true,
        maintainAspectRatio: false,
      maintainAspectRatio: false,
        interaction: { mode: 'nearest', intersect: false },
      interaction: { mode: 'nearest', intersect: false },
         plugins: {
      scales: {
          legend: { position: 'bottom', labels: { font: { size: 13 }, boxWidth: 20 } },
         x: { ticks: { font: { size: 12 } } },
          title:  { display: !!title, text: title, font: { size: 16 } },
        y: { beginAtZero: true, ticks: { precision: 0, font: { size: 12 } } }
          tooltip:{ backgroundColor: 'rgba(0,0,0,0.8)', titleFont: {size:14}, bodyFont: {size:13} }
      },
        },
      plugins: {
        scales: {
        legend: { position: 'bottom', labels: { font: { size: 13 }, boxWidth: 20 } },
          x: { ticks: { font: { size: 12 } } },
        title:  { display: !!title, text: title, font: { size: 16 } },
          y: { beginAtZero: true, ticks: { precision: 0, font: { size: 12 } } }
        tooltip:{ backgroundColor: 'rgba(0,0,0,0.8)', titleFont: {size:14}, bodyFont: {size:13} }
        }
      },
      }
      elements: { line: { tension: .25, borderWidth: 2 }, point: { radius: 3 } }
     });
     }
 
    // markiere als gerendert (verhindert Doppelaufbau)
    block.dataset.rendered = '1';
   });
   });
}


   // --- NEU: Total je nach Attribut anzeigen/ausblenden ---
   // 5) Start: erst wenn DOM fertig, dann Chart.js laden, dann rendern
   const hideTotal = isFlagTrue(block.dataset.hideTotal);
   function boot($scope){
 
    var blocks = ($scope && $scope[0] ? $scope[0] : document).querySelectorAll('.ados-chart-multi');
  // Falls bereits eine Zeile existiert: erst mal entfernen
    if (!blocks.length) return;
  const oldInfo = block.querySelector(':scope > .chart-total-info');
     ensureChartJS().then(function(){ blocks.forEach(renderOne); });
  if (oldInfo) oldInfo.remove();
 
  if (!hideTotal) {
     addTotalBelowLegend(chart);
    if (window.ResizeObserver) {
      const obs = new ResizeObserver(() => addTotalBelowLegend(chart));
      obs.observe(chart.canvas);
      chart.$adosTotalObserver = obs; // optional merken
    }
  } else {
    // Sicherheitshalber evtl. alten Observer trennen
    if (chart.$adosTotalObserver) {
      chart.$adosTotalObserver.disconnect();
      chart.$adosTotalObserver = null;
    }
   }
   }


  block.dataset.rendered = '1';
});
  // ---- Auto-Init auf jeder Seite ------------------------------------------
   if (window.mw && mw.hook) {
   if (window.mw && mw.hook) {
     mw.hook('wikipage.content').add(function ($c) {
     mw.hook('wikipage.content').add(boot);
      const scope = ($c && $c[0]) ? $c[0] : document;
      scope.querySelectorAll('.ados-chart-multi').forEach(renderMultiChart);
    });
   } else {
   } else {
     document.addEventListener('DOMContentLoaded', function () {
     // Fallback
       document.querySelectorAll('.ados-chart-multi').forEach(renderMultiChart);
    (document.readyState === 'loading')
    });
       ? document.addEventListener('DOMContentLoaded', function(){ boot(); })
      : boot();
   }
   }
})();
})();






// ============================================================
// ============================================================