let chartLine, chartBar, chartDonut; let allMonths = []; function getThemeColors() { const theme = document.documentElement.getAttribute('data-theme'); const isDark = theme === 'tokyo'; return { isDark, textColor: isDark ? '#e8f4ff' : '#1a1d22', gridColor: isDark ? 'rgba(100, 220, 255, 0.1)' : 'rgba(26, 29, 34, 0.1)', accent: isDark ? '#64dcff' : '#2c5a7a', secondary: isDark ? '#2c5a7a' : '#64dcff', palette: isDark ? ['#64dcff', '#2c5a7a', '#a3b3cc', '#7886a0', '#d93d3d', '#dcad2b', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4'] : ['#2c5a7a', '#64dcff', '#595f68', '#9299a2', '#a03030', '#8a6a10', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4'], bgTransparent: 'transparent', }; } function getCommonOptions() { const c = getThemeColors(); return { backgroundColor: c.bgTransparent, textStyle: { fontFamily: 'JetBrains Mono', color: c.textColor }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, legend: { textStyle: { color: c.textColor }, top: 10 }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', axisLine: { lineStyle: { color: c.gridColor } }, axisLabel: { color: c.textColor }, data: [], }, yAxis: { type: 'value', splitLine: { lineStyle: { color: c.gridColor } }, axisLabel: { color: c.textColor }, }, }; } function initCharts() { if (chartLine) chartLine.dispose(); if (chartBar) chartBar.dispose(); if (chartDonut) chartDonut.dispose(); chartLine = echarts.init(document.getElementById('chart-line')); chartBar = echarts.init(document.getElementById('chart-bar')); chartDonut = echarts.init(document.getElementById('chart-donut')); renderLineChart([], [], []); renderBarChart([], []); renderDonutChart([]); window.addEventListener('resize', () => { chartLine.resize(); chartBar.resize(); chartDonut.resize(); }); } function renderLineChart(months, massData, valueData) { const c = getThemeColors(); chartLine.setOption({ ...getCommonOptions(), tooltip: { trigger: 'axis' }, xAxis: { ...getCommonOptions().xAxis, data: months }, series: [ { name: 'Mass (kg)', type: 'line', smooth: true, itemStyle: { color: c.accent }, areaStyle: { opacity: 0.08 }, data: massData, }, { name: 'Value (DKK)', type: 'line', smooth: true, itemStyle: { color: c.secondary }, areaStyle: { opacity: 0.08 }, data: valueData, }, ], }); } function renderDonutChart(data) { const c = getThemeColors(); chartDonut.setOption({ backgroundColor: c.bgTransparent, textStyle: { fontFamily: 'JetBrains Mono', color: c.textColor }, tooltip: { trigger: 'item' }, legend: { orient: 'vertical', left: 'left', textStyle: { color: c.textColor } }, series: [ { type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 5, borderColor: c.isDark ? '#07070d' : '#f4f1ec', borderWidth: 2, }, label: { show: false, position: 'center' }, emphasis: { label: { show: true, fontSize: 16, fontWeight: 'bold', color: c.textColor }, }, data: data, color: c.palette, }, ], }); } function renderBarChart(months, seriesData) { const c = getThemeColors(); chartBar.setOption({ ...getCommonOptions(), xAxis: { ...getCommonOptions().xAxis, data: months }, series: seriesData, color: c.palette, }); } async function fetchData(endpoint) { try { const res = await fetch(endpoint); if (!res.ok) throw new Error(`API Error: ${res.status}`); return await res.json(); } catch (err) { console.error(err); return null; } } function populateSelect(id, items, codeKey, labelKey) { const sel = document.getElementById(id); sel.innerHTML = ''; items.forEach((item) => { const opt = document.createElement('option'); opt.value = item[codeKey]; opt.textContent = item[labelKey]; sel.appendChild(opt); }); } async function populateDropdowns() { showLoading(true); const [speciesList, zonesList, gearList, summaryData] = await Promise.all([ fetchData('/api/species'), fetchData('/api/zones'), fetchData('/api/gear'), fetchData('/api/summary'), ]); if (speciesList) { populateSelect('species', speciesList, 'code', 'label'); } if (zonesList) { populateSelect('zone', zonesList, 'code', 'label'); } if (gearList) { populateSelect('gear', gearList, 'code', 'label'); } if (summaryData && summaryData.monthly) { allMonths = summaryData.monthly.map((m) => m.month).sort(); const fillMonthSelect = (selectId) => { const sel = document.getElementById(selectId); sel.innerHTML = ''; allMonths.forEach((m) => { const opt = document.createElement('option'); opt.value = m; opt.textContent = m; sel.appendChild(opt); }); }; fillMonthSelect('month_from'); fillMonthSelect('month_to'); } showLoading(false); } async function updateCharts() { showLoading(true); hideEmptyState(); const species = document.getElementById('species').value; const zone = document.getElementById('zone').value; const gear = document.getElementById('gear').value; const monthFrom = document.getElementById('month_from').value; const monthTo = document.getElementById('month_to').value; const params = new URLSearchParams(); if (species) params.append('species', species); if (zone) params.append('zone', zone); if (gear) params.append('gear', gear); if (monthFrom) params.append('month_from', monthFrom); if (monthTo) params.append('month_to', monthTo); params.append('limit', '10000'); const [landings, summary] = await Promise.all([ fetchData(`/api/landings?${params.toString()}`), fetchData(`/api/summary?${species ? `species=${species}` : ''}`), ]); showLoading(false); if (!landings || landings.length === 0) { showEmptyState(); return; } hideEmptyState(); document.getElementById('charts-grid').style.display = 'grid'; // --- Line Chart --- const months = [...new Set(landings.map((l) => l.month))].sort(); const massMap = {}; const valueMap = {}; landings.forEach((l) => { if (!massMap[l.month]) massMap[l.month] = 0; if (!valueMap[l.month]) valueMap[l.month] = 0; if (l.measure_code === 'MASS' && l.value) massMap[l.month] += l.value; if (l.measure_code === 'VALUE' && l.value) valueMap[l.month] += l.value; }); renderLineChart( months, months.map((m) => massMap[m] || 0), months.map((m) => valueMap[m] || 0) ); // --- Donut Chart (Top Species by Value) --- const speciesValueMap = {}; landings.forEach((l) => { if (l.measure_code === 'VALUE' && l.value) { speciesValueMap[l.species_label] = (speciesValueMap[l.species_label] || 0) + l.value; } }); const donutData = Object.entries(speciesValueMap) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([name, value]) => ({ name, value })); renderDonutChart(donutData); // --- Bar Chart (Stacked by Species) --- const barSpeciesSet = new Set(); landings.forEach((l) => { if (l.measure_code === 'VALUE') barSpeciesSet.add(l.species_label); }); const barSpecies = [...barSpeciesSet].slice(0, 10); const barSeries = barSpecies.map((sp) => ({ name: sp, type: 'bar', stack: 'total', emphasis: { focus: 'series' }, data: months.map((m) => { const row = landings.find( (l) => l.month === m && l.species_label === sp && l.measure_code === 'VALUE' ); return row && row.value ? row.value : 0; }), })); renderBarChart(months, barSeries); } function showLoading(show) { const overlay = document.getElementById('loading-overlay'); if (show) { overlay.classList.remove('hidden'); } else { overlay.classList.add('hidden'); } } function showEmptyState() { document.getElementById('charts-grid').style.display = 'none'; document.getElementById('empty-state').style.display = 'flex'; } function hideEmptyState() { document.getElementById('empty-state').style.display = 'none'; } document.getElementById('apply-filters').addEventListener('click', updateCharts); // Theme Logic (() => { function currentSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'tokyo' : 'light'; } const btn = document.getElementById('theme-toggle'); if (btn) { btn.addEventListener('click', () => { const current = document.documentElement.getAttribute('data-theme') || 'light'; const next = current === 'light' ? 'tokyo' : 'light'; document.documentElement.setAttribute('data-theme', next); localStorage.setItem('poc-theme', next); initCharts(); updateCharts(); }); } window .matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', () => { if (!localStorage.getItem('poc-theme')) { document.documentElement.setAttribute('data-theme', currentSystemTheme()); initCharts(); updateCharts(); } }); })(); // Init initCharts(); populateDropdowns().then(updateCharts);