feat: add linear regression trendline to revenue charts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Line data={trendData} options={{...baseOptions, scales: {...baseOptions.scales, x: {...baseOptions.scales.x, ticks: {...baseOptions.scales.x.ticks, maxTicksLimit: trendGranularity === 'week' ? 15 : 20}}}}} />
|
||||
<Line data={trendData} options={{...baseOptions, plugins: {...baseOptions.plugins, legend: {display: true, position: 'top', align: 'end', labels: {boxWidth: 12, padding: 12, font: {size: 13}}}}, scales: {...baseOptions.scales, x: {...baseOptions.scales.x, ticks: {...baseOptions.scales.x.ticks, maxTicksLimit: trendGranularity === 'week' ? 15 : 20}}}}} />
|
||||
</ExportableChart>
|
||||
</div>
|
||||
|
||||
@@ -556,7 +587,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
|
||||
<button className={trendGranularity === 'week' ? 'active' : ''} onClick={() => setTrendGranularity('week')}>{t('time.weekly')}</button>
|
||||
</div>
|
||||
<div className="chart-container">
|
||||
<Line data={trendData} options={{...baseOptions, scales: {...baseOptions.scales, x: {...baseOptions.scales.x, ticks: {...baseOptions.scales.x.ticks, maxTicksLimit: 8}}}}} />
|
||||
<Line data={trendData} options={{...baseOptions, plugins: {...baseOptions.plugins, legend: {display: true, position: 'top', align: 'end', labels: {boxWidth: 10, padding: 8, font: {size: 12}}}}, scales: {...baseOptions.scales, x: {...baseOptions.scales.x, ticks: {...baseOptions.scales.x.ticks, maxTicksLimit: 8}}}}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user