From e92f11241d82fe2394c91adb81d942fe300b45c3 Mon Sep 17 00:00:00 2001 From: fahed Date: Mon, 9 Feb 2026 10:56:49 +0300 Subject: [PATCH] feat: add linear regression trendline to revenue charts Co-Authored-By: Claude Opus 4.6 --- src/components/Dashboard.tsx | 43 +++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 22465ec..4f8ca76 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -106,14 +106,44 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }; + // Linear regression helper + const linearRegression = (values: number[]) => { + const n = values.length; + if (n < 2) return values; + let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; + for (let i = 0; i < n; i++) { + sumX += i; + sumY += values[i]; + sumXY += i * values[i]; + sumX2 += i * i; + } + const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); + const intercept = (sumY - slope * sumX) / n; + return values.map((_, i) => slope * i + intercept); + }; + + const trendlineDataset = (values: number[]) => ({ + label: 'Trend', + data: linearRegression(values), + borderColor: chartColors.secondary, + borderWidth: 2, + borderDash: [6, 4], + tension: 0, + fill: false, + pointRadius: 0, + pointHoverRadius: 0, + datalabels: { display: false } + }); + if (trendGranularity === 'week') { const grouped = groupByWeek(filteredData, includeVAT); const weeks = Object.keys(grouped).filter(w => w).sort(); + const revenueValues = weeks.map(w => grouped[w].revenue); return { labels: weeks.map(formatLabel), datasets: [{ label: includeVAT ? 'Revenue (incl. VAT)' : 'Revenue (excl. VAT)', - data: weeks.map(w => grouped[w].revenue), + data: revenueValues, borderColor: chartColors.primary, backgroundColor: chartColors.primary + '10', borderWidth: 2, @@ -121,7 +151,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc fill: true, pointRadius: 0, pointHoverRadius: 4 - }] + }, trendlineDataset(revenueValues)] }; } else { // Daily granularity @@ -132,11 +162,12 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc dailyData[date] += Number(row[revenueField] || row.revenue_incl_tax || 0); }); const days = Object.keys(dailyData).sort(); + const revenueValues = days.map(d => dailyData[d]); return { labels: days.map(formatLabel), datasets: [{ label: includeVAT ? 'Revenue (incl. VAT)' : 'Revenue (excl. VAT)', - data: days.map(d => dailyData[d]), + data: revenueValues, borderColor: chartColors.primary, backgroundColor: chartColors.primary + '10', borderWidth: 1.5, @@ -144,7 +175,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc fill: true, pointRadius: 0, pointHoverRadius: 3 - }] + }, trendlineDataset(revenueValues)] }; } }, [filteredData, trendGranularity, includeVAT]); @@ -465,7 +496,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc } > - + @@ -556,7 +587,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
- +