From 1c281c4dc8d7efb6875e004e7fa5d6096b5d5cec Mon Sep 17 00:00:00 2001 From: Bartal Laearsson Date: Wed, 26 Aug 2026 00:09:11 +0100 Subject: [PATCH] frontend done --- static/app.js | 271 +++++++++++++++++++++++++--------------------- static/index.html | 18 ++- static/style.css | 20 ++++ 3 files changed, 180 insertions(+), 129 deletions(-) diff --git a/static/app.js b/static/app.js index 83faee8..3e2b4a6 100644 --- a/static/app.js +++ b/static/app.js @@ -1,4 +1,4 @@ -let chartLine, chartBar, chartDonut; +let chartLine, chartPrice, chartBar; let allMonths = []; const MONTHS_TO_SHOW_DEFAULT = 6; @@ -26,6 +26,13 @@ function formatDKKShort(value) { return value.toString(); } +function formatPricePerKg(value) { + return new Intl.NumberFormat('da-DK', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value) + ' DKK/kg'; +} + function getNextUpdateDate() { const now = new Date(); const currentMonth = now.getMonth(); @@ -99,13 +106,13 @@ function getThemeColors() { '#89b4fa', '#fab387', '#a6e3a1', - '#f9e2af', - '#f5c2e7', - '#cba6f7', - '#f38ba8', - '#eba0ac', - '#94e2d5', - '#89dceb', + '#f9e2af', + '#f5c2e7', + '#cba6f7', + '#f38ba8', + '#eba0ac', + '#94e2d5', + '#89dceb', ], tooltipBg: 'rgba(255, 255, 255, 0.98)', tooltipBorder: 'rgba(73, 77, 113, 0.15)', @@ -115,21 +122,27 @@ function getThemeColors() { function initCharts() { if (chartLine) chartLine.dispose(); + if (chartPrice) chartPrice.dispose(); if (chartBar) chartBar.dispose(); - if (chartDonut) chartDonut.dispose(); chartLine = echarts.init(document.getElementById('chart-line')); + chartPrice = echarts.init(document.getElementById('chart-price')); chartBar = echarts.init(document.getElementById('chart-bar')); - chartDonut = echarts.init(document.getElementById('chart-donut')); - renderLineChart([], [], []); - renderBarChart([], []); - renderTopSpeciesChart([]); + chartLine.setOption({ series: [] }); + chartPrice.setOption({ series: [] }); + chartBar.setOption({ series: [] }); + + setTimeout(() => { + chartLine.resize(); + chartPrice.resize(); + chartBar.resize(); + }, 0); window.addEventListener('resize', () => { chartLine.resize(); + chartPrice.resize(); chartBar.resize(); - chartDonut.resize(); }); } @@ -260,103 +273,83 @@ function renderLineChart(months, massData, valueData) { }); } -function renderTopSpeciesChart(data) { +function renderPriceChart(priceTrend) { const c = getThemeColors(); - const top5 = data.slice(0, 5); - - if (top5.length === 0) { - chartDonut.setOption({ series: [] }); - return; - } - - const sorted = [...top5].sort((a, b) => a.value - b.value); - const speciesLabels = sorted.map(d => d.name); - const values = sorted.map(d => d.value); - - chartDonut.setOption({ + chartPrice.setOption({ backgroundColor: c.bgTransparent, textStyle: { fontFamily: 'Noto Sans', color: c.textColor }, tooltip: { trigger: 'axis', - axisPointer: { type: 'shadow' }, + axisPointer: { type: 'cross' }, backgroundColor: c.tooltipBg, borderColor: c.tooltipBorder, borderWidth: 1, textStyle: { color: c.textColor, fontFamily: 'Noto Sans' }, formatter: (params) => { const p = params[0]; - return `${p.name}
${formatDKK(p.value)}`; + return `${p.axisValue}
${formatPricePerKg(p.value)}`; }, }, grid: { - left: '5%', - right: '20%', - bottom: 60, - top: 10, + left: '8%', + right: '10%', + bottom: 50, containLabel: true, + top: '18%', }, xAxis: { + type: 'category', + data: priceTrend.map(d => formatMonthDisplay(d.month)), + axisLine: { lineStyle: { color: c.gridColor } }, + axisLabel: { + color: c.subTextColor, + interval: priceTrend.length > 24 ? 6 : (priceTrend.length > 12 ? 3 : 1), + fontSize: 10, + rotate: 0, + margin: 8, + }, + }, + yAxis: { type: 'value', + name: 'Price per kg (DKK)', splitLine: { lineStyle: { color: c.gridColor, type: 'dashed' } }, axisLabel: { color: c.subTextColor, - formatter: (val) => formatDKKShort(val), - fontSize: 9, - hideOverlap: true, - }, - axisLine: { show: false }, - axisTick: { show: false }, - }, - yAxis: { - type: 'category', - data: speciesLabels, - axisLine: { lineStyle: { color: c.gridColor } }, - axisTick: { show: false }, - axisLabel: { - color: c.textColor, - fontSize: 11, - fontWeight: 'bold', - width: 100, - overflow: 'truncate', - interval: 'auto', + formatter: (val) => formatPricePerKg(val), + margin: 8, }, + axisLine: { show: true, lineStyle: { color: c.gridColor } }, + nameTextStyle: { color: c.accent, padding: [0, 0, 0, 0] }, }, series: [ { - type: 'bar', - data: values.map((v, i) => ({ - value: v, - itemStyle: { - color: c.palette[i], - borderRadius: [0, 4, 4, 0], - }, - })), - barMaxWidth: 28, - barGap: '30%', - label: { - show: true, - position: 'right', - formatter: (params) => formatDKKShort(params.value), - color: c.subTextColor, - fontSize: 10, - distance: 8, - overflow: 'break', - }, - emphasis: { - itemStyle: { - shadowBlur: 0, + name: 'Price per kg', + type: 'line', + smooth: true, + symbol: 'none', + lineStyle: { width: 3 }, + itemStyle: { color: c.secondary }, + areaStyle: { + color: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: c.isDark ? 'rgba(148, 226, 213, 0.3)' : 'rgba(148, 226, 213, 0.2)' }, + { offset: 1, color: 'rgba(148, 226, 213, 0)' }, + ], }, }, + data: priceTrend.map(d => d.price_per_kg), }, ], }); } -function renderBarChart(months, seriesData) { +function renderBarChart(months, breakdown, showOthers) { const c = getThemeColors(); - if (months.length === 0 || seriesData.length === 0) { + if (months.length === 0 || breakdown.length === 0) { chartBar.setOption({ series: [] }); return; } @@ -368,6 +361,49 @@ function renderBarChart(months, seriesData) { idx % step === 0 ? label : '' ); + const speciesMonthlyMap = {}; + const speciesLabels = {}; + const speciesTotals = {}; + + breakdown.forEach(row => { + if (!speciesMonthlyMap[row.species_code]) { + speciesMonthlyMap[row.species_code] = {}; + speciesLabels[row.species_code] = row.species_label || row.species_code; + speciesTotals[row.species_code] = 0; + } + speciesMonthlyMap[row.species_code][row.month] = row.total_value || 0; + speciesTotals[row.species_code] += row.total_value || 0; + }); + + const rankedSpecies = Object.entries(speciesTotals) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([code]) => code); + + const otherValues = months.map(m => { + return Object.keys(speciesMonthlyMap) + .filter(code => !rankedSpecies.includes(code)) + .reduce((sum, code) => sum + (speciesMonthlyMap[code][m] || 0), 0); + }); + + let seriesData = rankedSpecies.map(code => ({ + name: speciesLabels[code], + type: 'bar', + stack: 'total', + emphasis: { focus: 'series' }, + data: months.map(m => speciesMonthlyMap[code][m] || 0), + })); + + if (showOthers) { + seriesData.push({ + name: 'Other species', + type: 'bar', + stack: 'total', + emphasis: { focus: 'series' }, + data: otherValues, + }); + } + const chartWidth = chartBar.getWidth(); const barMaxWidth = Math.max(15, Math.floor((chartWidth * 0.7) / (months.length * 1.5))); @@ -384,9 +420,11 @@ function renderBarChart(months, seriesData) { formatter: (params) => { const month = params[0]?.axisValue || ''; let html = `${month}
`; - params.forEach(p => { + [...params].reverse().forEach(p => { const val = p.value || 0; - html += `${p.marker} ${p.seriesName}: ${formatDKK(val)}
`; + if (val > 0) { + html += `${p.marker} ${p.seriesName}: ${formatDKK(val)}
`; + } }); return html; }, @@ -397,7 +435,7 @@ function renderBarChart(months, seriesData) { left: 'center', orient: 'horizontal', type: 'scroll', - maxItems: 5, + maxItems: 8, padding: [0, 0, 0, 0], itemGap: 10, }, @@ -678,69 +716,53 @@ async function updateCharts() { } const months = summary.monthly.map(m => m.month).sort(); - const topSpecies = summary.top_species ? summary.top_species.slice(0, 5) : []; - if (monthlyBreakdown && monthlyBreakdown.length > 0 && topSpecies.length > 0) { - const speciesMonthlyMap = {}; + const showAllSpecies = document.getElementById('show-all-species')?.checked ?? false; - monthlyBreakdown.forEach(row => { - if (!speciesMonthlyMap[row.species_code]) { - speciesMonthlyMap[row.species_code] = {}; - } - speciesMonthlyMap[row.species_code][row.month] = row.total_value || 0; - }); - - const topSpeciesCodes = topSpecies.map(s => s.species_code); - - const filteredBreakdown = monthlyBreakdown.filter(row => - topSpeciesCodes.includes(row.species_code) - ); - - const barSeries = topSpecies.map(sp => { - const data = months.map(month => { - const matching = filteredBreakdown.find( - row => row.month === month && row.species_code === sp.species_code - ); - return matching ? matching.total_value || 0 : 0; - }); - - return { - name: sp.species_label, - type: 'bar', - stack: 'total', - emphasis: { focus: 'series' }, - data: data, - }; - }); - - renderBarChart(months, barSeries); + if (monthlyBreakdown && monthlyBreakdown.length > 0) { + renderBarChart(months, monthlyBreakdown, showAllSpecies); } else { - renderBarChart([], []); + renderBarChart([], [], false); } const massData = summary.monthly.map((m) => m.total_mass || 0); const valueData = summary.monthly.map((m) => m.total_value || 0); renderLineChart(months, massData, valueData); - if (summary.top_species && summary.top_species.length > 0) { - const topData = summary.top_species.map((s) => ({ - name: s.species_label, - value: s.total_value || 0, - })); - renderTopSpeciesChart(topData); + const priceTrend = summary.price_trend ?? []; + if (priceTrend.length > 0) { + renderPriceChart(priceTrend); } else { - renderTopSpeciesChart([]); + chartPrice.setOption({ series: [] }); } showLoading(false); hideEmptyState(); document.getElementById('charts-grid').style.display = 'grid'; + chartLine.resize(); + chartPrice.resize(); + chartBar.resize(); + + renderLineChart(months, massData, valueData); + + if (priceTrend.length > 0) { + renderPriceChart(priceTrend); + } else { + chartPrice.setOption({ series: [] }); + } + + if (monthlyBreakdown && monthlyBreakdown.length > 0) { + renderBarChart(months, monthlyBreakdown, showAllSpecies); + } else { + renderBarChart([], [], false); + } + setTimeout(() => { chartLine.resize(); + chartPrice.resize(); chartBar.resize(); - chartDonut.resize(); - }, 0); + }, 50); } function showLoading(show) { @@ -758,11 +780,14 @@ function showEmptyState() { } function hideEmptyState() { + document.getElementById('charts-grid').style.display = 'grid'; document.getElementById('empty-state').style.display = 'none'; } document.getElementById('apply-filters').addEventListener('click', updateCharts); +document.getElementById('show-all-species')?.addEventListener('change', updateCharts); + document.addEventListener('click', (e) => { if (!e.target.closest('.filter-group.multi-select')) { document.querySelectorAll('.checkbox-dropdown-list.open').forEach(el => { diff --git a/static/index.html b/static/index.html index 02dd0f8..4e9e350 100644 --- a/static/index.html +++ b/static/index.html @@ -13,9 +13,9 @@ })(); - - - + + + @@ -57,6 +57,12 @@ +
+ +
@@ -75,12 +81,12 @@