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 landingsParams = new URLSearchParams(); if (species) landingsParams.append('species', species); if (zone) landingsParams.append('zone', zone); if (gear) landingsParams.append('gear', gear); if (monthFrom) landingsParams.append('month_from', monthFrom); if (monthTo) landingsParams.append('month_to', monthTo); landingsParams.append('limit', '10000'); const summaryParams = new URLSearchParams(); if (species) summaryParams.append('species', species); if (monthFrom) summaryParams.append('month_from', monthFrom); if (monthTo) summaryParams.append('month_to', monthTo); const [landings, summary] = await Promise.all([ fetchData(`/api/landings?${landingsParams.toString()}`), fetchData(`/api/summary?${summaryParams.toString()}`), ]); showLoading(false); if ((!landings || landings.length === 0) && (!summary || !summary.monthly || summary.monthly.length === 0)) { showEmptyState(); return; } hideEmptyState(); document.getElementById('charts-grid').style.display = 'grid'; setTimeout(() => { chartLine.resize(); chartBar.resize(); chartDonut.resize(); }, 0); // --- Line Chart (from filtered summary) --- if (summary && summary.monthly) { const months = summary.monthly.map((m) => m.month); const massData = summary.monthly.map((m) => m.total_mass || 0); const valueData = summary.monthly.map((m) => m.total_value || 0); renderLineChart(months, massData, valueData); } else { renderLineChart([], [], []); } // --- Donut Chart (from filtered summary top_species) --- if (summary && summary.top_species && summary.top_species.length > 0) { const donutData = summary.top_species.slice(0, 10).map((s) => ({ name: s.species_label, value: s.total_value || 0, })); renderDonutChart(donutData); } else { renderDonutChart([]); } // --- Bar Chart (from filtered landings) --- if (landings && landings.length > 0) { const barSpeciesSet = new Set(); landings.forEach((l) => { if (l.measure_code === 'VALUE') barSpeciesSet.add(l.species_label); }); const barSpecies = [...barSpeciesSet].slice(0, 10); const months = [...new Set(landings.map((l) => l.month))].sort(); 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); } else { renderBarChart([], []); } } 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 document.addEventListener('DOMContentLoaded', () => { initCharts(); populateDropdowns().then(updateCharts); });