MediaWiki:Common.js: Unterschied zwischen den Versionen

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


/* === ADOS Multi-Serien-Chart (Chart.js) ============================= *
/* === ADOS Multi-Serien-Chart (Chart.js + Cargo-Table) ===================== */
* Lädt Chart.js sicher (asynchron) und baut Diagramme aus Cargo-Tabellen.
/* Fügt unter der Legende automatisch "Gesamt: N" ein und kann die Roh-Tabelle
* Benötigt im Artikel: <div class="ados-chart-multi"> + Cargo-Query als |format=table
  ausblenden (data-hide-table="true" am Container).
* ==================================================================== */
  Nutzung im Wikitext:
  <div class="ados-chart-multi" data-type="line|bar" data-title="…" data-hide-table="true"></div>
  {{#cargo_query: … |fields=Erscheinungsjahr=Jahr, Serie, COUNT(*)=Anzahl |group by=Jahr,Serie |format=table}}
*/


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


   // 2) Farben je Serie (gut unterscheidbar)
   // ---- Farbpalette (gut unterscheidbar, auch mobil & dark mode) -----------
   var ADOS_COLORS = {
   const ADOS_COLORS = {
     'A Dream of Scotland':                 '#C2410C', // Kupferbraun
     'A Dream of Scotland':               '#C2410C', // Kupfer
     'A Dream of Ireland':                 '#15803D', // Flaschengrün
     'A Dream of Ireland':               '#15803D', // Grün
     'A Dream of... – Der Rest der Welt':   '#1D4ED8', // Mittelblau
     'A Dream of... – Der Rest der Welt': '#1D4ED8', // Blau
     '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'  // Goldgelb
     'The Fine Art of Whisky':           '#CA8A04'  // Gold
   };
   };
   var COLOR_CYCLE = ['#2563eb','#16a34a','#f97316','#dc2626','#a855f7','#0ea5e9','#f59e0b','#10b981'];
   const COLOR_CYCLE = ['#2563EB','#16A34A','#F97316','#DC2626','#A855F7','#0EA5E9','#F59E0B','#10B981'];


   function toYear(x){
  // ---- Helpers -------------------------------------------------------------
     var n = parseInt(String(x).replace(/[^\d]/g,''),10);
   function toYear(x) {
     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];
     var i = used.size % COLOR_CYCLE.length;
     const idx = used.size % COLOR_CYCLE.length;
     used.add(name);
     used.add(name);
     return COLOR_CYCLE[i];
     return COLOR_CYCLE[idx];
   }
   }


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


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


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


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


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


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


  // 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;
      const container = chart.canvas.parentNode; // wrapper div
    } else if (el.querySelector) {
      let info = container.querySelector('.chart-total-info');
      var t = el.querySelector('table');
      if (!info) {
       if (t) tbl = t;
        info = document.createElement('div');
     }
        info.className = 'chart-total-info';
    if (tbl) {
        info.style.textAlign = 'center';
       // Wrapper merken – aber später nur verstecken, wenn er "quasi nur" die Tabelle enthält
        info.style.fontWeight = 'bold';
      wrapToHide = tbl.parentElement;
        info.style.fontSize = '1.05em';
      break;
        info.style.marginTop = '0.4em';
        info.style.color = '#444';
        container.appendChild(info);
       }
      info.textContent = 'Gesamt: ' + total;
     } catch (e) {
       console.warn('[chart-total]', e);
     }
     }
    el = el.nextElementSibling;
   }
   }
  if (!tbl) return;


   var out = buildDatasetsFromTable(tbl);
   // ---- Einzel-Chart rendern -----------------------------------------------
  if (!out.labels.length || !out.datasets.length) return;
  function renderOne(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;


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


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


  var type  = (block.dataset.type || 'line').toLowerCase();
    // Chart-Optionen
  var title = block.dataset.title || '';
    const type  = (block.dataset.type || 'line').toLowerCase(); // "line"|"bar"
  var cumulative = (block.dataset.cumulative || '').toLowerCase() === 'true';
    const title = (block.dataset.title || '');


  // optional: kumulative Werte pro Serie bauen
    ensureChartJS().then(function () {
  if (cumulative) {
      const chart = new Chart(canvas.getContext('2d'), {
    out.datasets = out.datasets.map(function(ds){
        type: type,
      var acc = 0;
        data: { labels: out.labels, datasets: out.datasets },
      return Object.assign({}, ds, {
        options: {
        data: ds.data.map(function(v){ acc += v; return acc; })
          responsive: true,
          maintainAspectRatio: false,
          interaction: { mode: 'nearest', intersect: false },
          scales: {
            x: { ticks: { font: { size: 12 } } },
            y: { beginAtZero: true, ticks: { precision: 0, font: { size: 12 } } }
          },
          plugins: {
            legend: { position: 'bottom', labels: { font: { size: 13 }, boxWidth: 20 } },
            title:  { display: !!title, text: title, font: { size: 16 } },
            tooltip:{ backgroundColor: 'rgba(0,0,0,0.8)', titleFont: {size:14}, bodyFont: {size:13} }
          },
          elements: { line: { tension: 0.25, borderWidth: 2 }, point: { radius: 3 } }
        }
       });
       });
    });
  }


  ensureChartJS().then(function(){
      // Gesamtzahl einfügen + bei Größenänderung neu berechnen
    new Chart(canvas.getContext('2d'), {
      addTotalBelowLegend(chart);
      type: type,
      if (window.ResizeObserver) {
      data: { labels: out.labels, datasets: out.datasets },
        const obs = new ResizeObserver(() => addTotalBelowLegend(chart));
      options: {
        obs.observe(chart.canvas);
        responsive: true,
        maintainAspectRatio: false,
        interaction: { mode: 'nearest', intersect: false },
        plugins: {
          legend: { position: 'bottom', labels: { font: { size: 13 }, boxWidth: 20 } },
          title:  { display: !!title, text: title, font: { size: 16 } },
          tooltip:{ backgroundColor: 'rgba(0,0,0,0.8)', titleFont: {size:14}, bodyFont: {size:13} }
        },
        scales: {
          x: { ticks: { font: { size: 12 } } },
          y: { beginAtZero: true, ticks: { precision: 0, font: { size: 12 } } }
        }
       }
       }
    });
    // markiere als gerendert (verhindert Doppelaufbau)
    block.dataset.rendered = '1';
  });
}


  // 5) Start: erst wenn DOM fertig, dann Chart.js laden, dann rendern
      block.dataset.rendered = '1';
  function boot($scope){
     }).catch(err => console.error('[ChartJS]', err));
    var blocks = ($scope && $scope[0] ? $scope[0] : document).querySelectorAll('.ados-chart-multi');
     if (!blocks.length) return;
    ensureChartJS().then(function(){ blocks.forEach(renderOne); });
   }
   }


  // ---- Auto-Init auf jeder Seite ------------------------------------------
   if (window.mw && mw.hook) {
   if (window.mw && mw.hook) {
     mw.hook('wikipage.content').add(boot);
     mw.hook('wikipage.content').add(function ($c) {
      const scope = ($c && $c[0]) ? $c[0] : document;
      scope.querySelectorAll('.ados-chart-multi').forEach(renderOne);
    });
   } else {
   } else {
     // Fallback
     // Fallback, falls mw.hook nicht verfügbar ist
     (document.readyState === 'loading')
     document.addEventListener('DOMContentLoaded', function () {
       ? document.addEventListener('DOMContentLoaded', function(){ boot(); })
       document.querySelectorAll('.ados-chart-multi').forEach(renderOne);
      : boot();
    });
   }
   }
})();
})();


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