MediaWiki:Common.js: Unterschied zwischen den Versionen
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung Markierung: Zurückgesetzt |
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung Markierung: Manuelle Zurücksetzung |
||
| Zeile 740: | Zeile 740: | ||
// ------------------------------------- | // ------------------------------------- | ||
/* === ADOS | /* === ADOS Multi-Serien-Chart (Chart.js) ============================= * | ||
* Lädt Chart.js sicher (asynchron) und baut Diagramme aus Cargo-Tabellen. | |||
* Benötigt im Artikel: <div class="ados-chart-multi"> + Cargo-Query als |format=table | |||
* ==================================================================== */ | |||
*/ | |||
(function () { | (function () { | ||
// 1) Chart.js nur 1x laden und erst dann rendern | |||
var _chartReady = null; | |||
// | function ensureChartJS() { | ||
function ensureChartJS( | if (_chartReady) return _chartReady; | ||
if (window.Chart) return | _chartReady = new Promise(function (resolve, reject) { | ||
if (window.Chart) return resolve(); | |||
var s = document.createElement('script'); | |||
s.src = 'https://cdn.jsdelivr.net/npm/chart.js'; // UMD-Bundle | |||
s.async = true; | |||
s.onload = function(){ resolve(); }; | |||
s.onerror = function(){ console.error('Chart.js konnte nicht geladen werden'); reject(); }; | |||
document.head.appendChild(s); | |||
}); | |||
return _chartReady; | |||
} | } | ||
// | // 2) Farben je Serie (gut unterscheidbar) | ||
var ADOS_COLORS = { | |||
'A Dream of Scotland': | 'A Dream of Scotland': '#C2410C', // Kupferbraun | ||
'A Dream of Ireland': | 'A Dream of Ireland': '#15803D', // Flaschengrün | ||
'A Dream of... – Der Rest der Welt': '#1D4ED8', // | 'A Dream of... – Der Rest der Welt': '#1D4ED8', // Mittelblau | ||
'Friendly Mr. Z': | 'Friendly Mr. Z': '#9333EA', // Violett | ||
'Die Whisky Elfen': | 'Die Whisky Elfen': '#0891B2', // Türkis | ||
'The Fine Art of Whisky': | 'The Fine Art of Whisky': '#CA8A04' // Goldgelb | ||
}; | }; | ||
var COLOR_CYCLE = ['#2563eb','#16a34a','#f97316','#dc2626','#a855f7','#0ea5e9','#f59e0b','#10b981']; | |||
function toYear(x){ | |||
function toYear(x) { | var 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; | |||
used.add(name); | used.add(name); | ||
return COLOR_CYCLE[ | return COLOR_CYCLE[i]; | ||
} | } | ||
// Tabelle -> {labels,datasets} | // 3) Tabelle (Jahr | Serie | Anzahl) -> {labels, datasets} | ||
function buildDatasetsFromTable(tbl) { | function buildDatasetsFromTable(tbl){ | ||
var rows = Array.from(tbl.querySelectorAll('tr')); | |||
if (rows.length < 2) return { labels: [], datasets: [] }; | if (rows.length < 2) return { labels:[], datasets:[] }; | ||
var yearsSet = new Set(); | |||
var bySeries = new Map(); // serie -> Map(year -> count) | |||
rows.slice(1).forEach(tr | rows.slice(1).forEach(function(tr){ | ||
var tds = tr.querySelectorAll('td,th'); | |||
if (tds.length < 3) return; | if (tds.length < 3) return; | ||
var y = toYear(tds[0].textContent.trim()); | |||
var s = tds[1].textContent.trim(); | |||
var v = parseFloat(tds[2].textContent.replace(',','.')) || 0; | |||
if (y == null) return; | if (y == null) return; | ||
yearsSet.add(y); | |||
if (! | if (!bySeries.has(s)) bySeries.set(s, new Map()); | ||
bySeries.get(s).set(y, v); | |||
}); | }); | ||
var years = Array.from(yearsSet).sort(function(a,b){return a-b;}); | |||
var used = new Set(); | |||
var labels = years.map(String); | |||
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, | ||
borderColor: | borderColor: color, | ||
backgroundColor: | 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 }; | ||
} | } | ||
// - | // 4) Einen Chart-Container rendern: nimmt die NÄCHSTE Tabelle als Datenquelle | ||
function | function renderOne(block){ | ||
// schon gerendert? (z. B. durch AJAX/Minerva-Reloads) | |||
if (block.dataset.rendered === '1') return; | |||
// nächste Tabelle (auch wenn sie in einem Wrapper steckt) finden | |||
var el = block.nextElementSibling, tbl = null, wrapToHide = null; | |||
while (el) { | |||
if (/^H[1-6]$/.test(el.tagName) || (el.classList && el.classList.contains('ados-chart-multi'))) break; | |||
if (el.tagName === 'TABLE') { | |||
tbl = el; | |||
// | } else if (el.querySelector) { | ||
var t = el.querySelector('table'); | |||
if (t) tbl = t; | |||
} | |||
if (tbl) { | |||
// Wrapper merken – aber später nur verstecken, wenn er "quasi nur" die Tabelle enthält | |||
wrapToHide = tbl.parentElement; | |||
if ( | break; | ||
// | |||
} | } | ||
el = el.nextElementSibling; | |||
} | } | ||
if (!tbl) return; | |||
var out = buildDatasetsFromTable(tbl); | |||
if (!out.labels.length || !out.datasets.length) return; | |||
// Tabelle verstecken, wenn gewünscht (nur die Tabelle – oder den Wrapper, falls er sonst leer ist) | |||
var hide = (block.dataset.hideTable || '').toLowerCase() === 'true'; | |||
if ( | 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'; | ||
} | } | ||
} | |||
// Zeichenfläche einsetzen (mobil/desktop) | |||
var wrap = document.createElement('div'); | |||
wrap.style.position = 'relative'; | |||
wrap.style.width = '100%'; | |||
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); | |||
var type = (block.dataset.type || 'line').toLowerCase(); | |||
var title = block.dataset.title || ''; | |||
var cumulative = (block.dataset.cumulative || '').toLowerCase() === 'true'; | |||
// optional: kumulative Werte pro Serie bauen | |||
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; }) | |||
}); | |||
}); | |||
} | |||
ensureChartJS().then(function(){ | |||
new Chart(canvas.getContext('2d'), { | |||
type: type, | |||
data: { labels: out.labels, datasets: out.datasets }, | |||
options: { | |||
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 | ||
function boot($scope){ | |||
var blocks = ($scope && $scope[0] ? $scope[0] : document).querySelectorAll('.ados-chart-multi'); | |||
if (!blocks.length) return; | |||
ensureChartJS().then(function(){ blocks.forEach(renderOne); }); | |||
} | } | ||
if (window.mw && mw.hook) { | if (window.mw && mw.hook) { | ||
mw.hook('wikipage.content').add( | mw.hook('wikipage.content').add(boot); | ||
} else { | } else { | ||
document. | // Fallback | ||
document. | (document.readyState === 'loading') | ||
? document.addEventListener('DOMContentLoaded', function(){ boot(); }) | |||
: boot(); | |||
} | } | ||
})(); | })(); | ||
// ============================================================ | // ============================================================ | ||