MediaWiki:Common.js: Unterschied zwischen den Versionen
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung Markierung: Manuelle Zurücksetzung |
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung Markierung: Zurückgesetzt |
||
| Zeile 740: | Zeile 740: | ||
// ------------------------------------- | // ------------------------------------- | ||
/* === ADOS Multi-Serien-Chart (Chart.js) | /* === ADOS Multi-Serien-Chart (Chart.js + Cargo-Table) ===================== */ | ||
/* Fügt unter der Legende automatisch "Gesamt: N" ein und kann die Roh-Tabelle | |||
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 () { | ||
// | 'use strict'; | ||
// ---- Chart.js Laden (als Promise) --------------------------------------- | |||
function ensureChartJS() { | function ensureChartJS() { | ||
if ( | if (window.Chart) return Promise.resolve(); | ||
return new Promise(function (resolve, reject) { | |||
var s = document.createElement('script'); | var s = document.createElement('script'); | ||
s.src = 'https://cdn.jsdelivr.net/npm/chart.js'; | s.src = 'https://cdn.jsdelivr.net/npm/chart.js'; | ||
s.async = true; | s.async = true; | ||
s.onload = | s.onload = () => resolve(); | ||
s.onerror = | s.onerror = () => reject(new Error('Chart.js konnte nicht geladen werden')); | ||
document.head.appendChild(s); | document.head.appendChild(s); | ||
}); | }); | ||
} | } | ||
// | // ---- Farbpalette (gut unterscheidbar, auch mobil & dark mode) ----------- | ||
const ADOS_COLORS = { | |||
'A Dream of Scotland': | 'A Dream of Scotland': '#C2410C', // Kupfer | ||
'A Dream of Ireland': | 'A Dream of Ireland': '#15803D', // Grün | ||
'A Dream of... – Der Rest der Welt': | 'A Dream of... – Der Rest der Welt': '#1D4ED8', // Blau | ||
'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' // Gold | ||
}; | }; | ||
const COLOR_CYCLE = ['#2563EB','#16A34A','#F97316','#DC2626','#A855F7','#0EA5E9','#F59E0B','#10B981']; | |||
function toYear(x){ | // ---- Helpers ------------------------------------------------------------- | ||
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]; | ||
const idx = used.size % COLOR_CYCLE.length; | |||
used.add(name); | used.add(name); | ||
return COLOR_CYCLE[ | return COLOR_CYCLE[idx]; | ||
} | } | ||
// | // Tabelle -> {labels,datasets} | ||
function buildDatasetsFromTable(tbl){ | function buildDatasetsFromTable(tbl) { | ||
const rows = Array.from(tbl.querySelectorAll('tr')); | |||
if (rows.length < 2) return { labels:[], datasets:[] }; | if (rows.length < 2) return { labels: [], datasets: [] }; | ||
const years = new Set(); | |||
const seriesMap = new Map(); // Serie -> Map(Jahr -> Wert) | |||
rows.slice(1).forEach | rows.slice(1).forEach(tr => { | ||
const tds = tr.querySelectorAll('td,th'); | |||
if (tds.length < 3) return; | if (tds.length < 3) return; | ||
const y = toYear(tds[0].textContent); | |||
const s = tds[1].textContent.trim(); | |||
const v = parseFloat(String(tds[2].textContent).replace(',', '.')) || 0; | |||
if (y == null) return; | if (y == null) return; | ||
years.add(y); | |||
if (! | if (!seriesMap.has(s)) seriesMap.set(s, new Map()); | ||
seriesMap.get(s).set(y, v); | |||
}); | }); | ||
const labels = Array.from(years).sort((a, b) => a - b).map(String); | |||
const used = new Set(); | |||
const datasets = Array.from(seriesMap.entries()).map(([name, ym]) => { | |||
const c = getColor(name, used); | |||
return { | return { | ||
label: name, | label: name, | ||
data: | data: labels.map(y => ym.get(+y) || 0), | ||
borderColor: | borderColor: c, | ||
backgroundColor: | backgroundColor: c + '80', | ||
tension: 0.25, | tension: 0.25, | ||
pointRadius: 3 | pointRadius: 3 | ||
| Zeile 822: | Zeile 821: | ||
}); | }); | ||
return { | return { labels, datasets }; | ||
} | } | ||
// | // ---- Zusatz: Gesamtzahl unter der Legende anzeigen ---------------------- | ||
function | function addTotalBelowLegend(chart) { | ||
try { | |||
if (!chart || !chart.data || !chart.data.datasets?.length) return; | |||
const total = chart.data.datasets.reduce((sum, ds) => { | |||
const arr = Array.isArray(ds.data) ? ds.data : []; | |||
return sum + arr.reduce((a, b) => a + (parseFloat(b) || 0), 0); | |||
}, 0); | |||
const container = chart.canvas.parentNode; // wrapper div | |||
let info = container.querySelector('.chart-total-info'); | |||
if (!info) { | |||
info = document.createElement('div'); | |||
} | info.className = 'chart-total-info'; | ||
info.style.textAlign = 'center'; | |||
info.style.fontWeight = 'bold'; | |||
info.style.fontSize = '1.05em'; | |||
info.style.marginTop = '0.4em'; | |||
info.style.color = '#444'; | |||
container.appendChild(info); | |||
} | |||
info.textContent = 'Gesamt: ' + total; | |||
} catch (e) { | |||
console.warn('[chart-total]', e); | |||
} | } | ||
} | } | ||
// ---- Einzel-Chart rendern ----------------------------------------------- | |||
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; | |||
// Daten lesen | |||
const out = buildDatasetsFromTable(tbl); | |||
if (!out.labels.length || !out.datasets.length) return; | |||
// Tabelle ggf. ausblenden (data-hide-table="true") | |||
const hide = (block.dataset.hideTable || '').toLowerCase() === 'true'; | |||
if (hide) { | |||
tbl.setAttribute('aria-hidden', 'true'); | |||
tbl.setAttribute('aria-hidden','true'); | |||
tbl.style.display = 'none'; | tbl.style.display = 'none'; | ||
} | } | ||
// Canvas-Wrapper | |||
const wrap = document.createElement('div'); | |||
wrap.style.position = 'relative'; | |||
wrap.style.width = '100%'; | |||
wrap.style.height = window.matchMedia('(min-width: 768px)').matches ? '450px' : '320px'; | |||
const canvas = document.createElement('canvas'); | |||
wrap.appendChild(canvas); | |||
block.innerHTML = ''; | |||
block.appendChild(wrap); | |||
// Chart-Optionen | |||
const type = (block.dataset.type || 'line').toLowerCase(); // "line"|"bar" | |||
const title = (block.dataset.title || ''); | |||
ensureChartJS().then(function () { | |||
const chart = new Chart(canvas.getContext('2d'), { | |||
type: type, | |||
data: { labels: out.labels, datasets: out.datasets }, | |||
options: { | |||
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 } } | |||
} | |||
}); | }); | ||
// Gesamtzahl einfügen + bei Größenänderung neu berechnen | |||
addTotalBelowLegend(chart); | |||
if (window.ResizeObserver) { | |||
const obs = new ResizeObserver(() => addTotalBelowLegend(chart)); | |||
obs.observe(chart.canvas); | |||
} | } | ||
block.dataset.rendered = '1'; | |||
}).catch(err => console.error('[ChartJS]', err)); | |||
} | } | ||
// ---- Auto-Init auf jeder Seite ------------------------------------------ | |||
if (window.mw && mw.hook) { | if (window.mw && mw.hook) { | ||
mw.hook('wikipage.content').add( | 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.addEventListener('DOMContentLoaded', function () { | |||
document.querySelectorAll('.ados-chart-multi').forEach(renderOne); | |||
}); | |||
} | } | ||
})(); | })(); | ||
// ============================================================ | // ============================================================ | ||