further refinement of roi calculator

This commit is contained in:
Tobias Brunner 2025-07-22 17:30:37 +02:00
parent 6f6c80480f
commit 493d45bb5d
Signed by: tobru
SSH key fingerprint: SHA256:kOXg1R6c11XW3/Pt9dbLdQvOJGFAy+B2K6v6PtRWBGQ
6 changed files with 962 additions and 565 deletions

View file

@ -84,7 +84,7 @@ class ChartManager {
});
// Model Comparison Chart (replaces generic Cash Flow Chart)
const modelComparisonCanvas = document.getElementById('cashFlowChart');
const modelComparisonCanvas = document.getElementById('modelComparisonChart');
if (!modelComparisonCanvas) {
console.error('Model comparison chart canvas not found');
return;
@ -98,7 +98,7 @@ class ChartManager {
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
title: { display: true, text: 'Investment Model Performance Comparison' }
title: { display: true, text: 'Investment Model Comparison' }
},
scales: {
y: {
@ -111,6 +111,35 @@ class ChartManager {
}
}
});
// Performance Comparison Chart (for cashFlowChart canvas)
const performanceCanvas = document.getElementById('cashFlowChart');
if (!performanceCanvas) {
console.error('Performance comparison chart canvas not found');
return;
}
const performanceCtx = performanceCanvas.getContext('2d');
this.charts.performance = new Chart(performanceCtx, {
type: 'bar',
data: { labels: [], datasets: [] },
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
title: { display: true, text: 'Model Performance Comparison' }
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: 'ROI (%)' }
},
x: {
title: { display: true, text: 'Growth Scenario' }
}
}
}
});
} catch (error) {
console.error('Error initializing charts:', error);
this.showChartError('Failed to initialize charts. Please refresh the page.');
@ -119,7 +148,7 @@ class ChartManager {
showChartError(message) {
// Show error message in place of charts
const chartContainers = ['instanceGrowthChart', 'revenueChart', 'cashFlowChart'];
const chartContainers = ['instanceGrowthChart', 'revenueChart', 'cashFlowChart', 'modelComparisonChart'];
chartContainers.forEach(containerId => {
const container = document.getElementById(containerId);
if (container) {
@ -152,7 +181,7 @@ class ChartManager {
// Update ROI Progression Chart
this.charts.roiProgression.data.labels = monthLabels;
this.charts.roiProgression.data.datasets = scenarios.map(scenario => ({
this.charts.roiProgression.data.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => ({
label: `${this.calculator.scenarios[scenario].name} (${this.calculator.results[scenario].investmentModel})`,
data: this.calculator.monthlyData[scenario].map(d => d.roiPercent),
borderColor: colors[scenario],
@ -166,7 +195,7 @@ class ChartManager {
// Update Net Position Chart (Break-Even Analysis)
this.charts.netPosition.data.labels = monthLabels;
this.charts.netPosition.data.datasets = scenarios.map(scenario => ({
this.charts.netPosition.data.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => ({
label: `${this.calculator.scenarios[scenario].name} Net Position`,
data: this.calculator.monthlyData[scenario].map(d => d.netPosition),
borderColor: colors[scenario],
@ -180,37 +209,122 @@ class ChartManager {
}));
this.charts.netPosition.update();
// Update Model Comparison Chart
const scenarioLabels = scenarios.map(s => this.calculator.scenarios[s].name);
const currentInvestmentModel = Object.values(this.calculator.results)[0]?.investmentModel || 'direct';
// Update Model Comparison Chart - Side-by-side comparison of both models
const inputs = this.calculator.getInputValues();
// Show comparison with both models for the same scenarios
this.charts.modelComparison.data.labels = scenarioLabels;
this.charts.modelComparison.data.datasets = [{
label: `${currentInvestmentModel === 'loan' ? 'Loan Model' : 'Direct Investment'} - Final Return`,
data: scenarios.map(scenario => this.calculator.results[scenario].cspRevenue),
backgroundColor: scenarios.map(scenario => colors[scenario] + '80'),
borderColor: scenarios.map(scenario => colors[scenario]),
borderWidth: 2
}];
// Calculate loan model net profit (fixed return regardless of scenario)
const loanMonthlyPayment = this.calculateLoanPayment(inputs.investmentAmount, inputs.loanInterestRate, inputs.timeframe);
const loanTotalPayments = loanMonthlyPayment * (inputs.timeframe * 12);
const loanNetProfit = loanTotalPayments - inputs.investmentAmount;
// Add performance metrics for direct investment
if (currentInvestmentModel === 'direct') {
this.charts.modelComparison.data.datasets.push({
label: 'Performance Bonus Impact',
data: scenarios.map(scenario =>
this.calculator.results[scenario].avgPerformanceBonus *
this.calculator.results[scenario].cspRevenue
),
backgroundColor: '#17a2b8',
borderColor: '#17a2b8',
// Prepare scenario-based comparison
const enabledScenarios = scenarios.filter(s => this.calculator.scenarios[s].enabled);
const comparisonLabels = enabledScenarios.map(s => this.calculator.scenarios[s].name);
// Loan model data (same profit for all scenarios since it's fixed)
const loanModelData = enabledScenarios.map(() => loanNetProfit);
// Direct investment data (varies by scenario performance)
const directInvestmentData = enabledScenarios.map(scenario => {
const scenarioResult = this.calculator.results[scenario];
if (!scenarioResult) return 0;
return scenarioResult.cspRevenue - inputs.investmentAmount;
});
// Performance bonus data (shows the additional revenue from performance bonuses)
const performanceBonusData = enabledScenarios.map(scenario => {
const monthlyData = this.calculator.monthlyData[scenario] || [];
const totalPerformanceBonus = monthlyData.reduce((sum, month) => {
// Calculate the bonus revenue (difference from standard share)
const standardRevenue = month.monthlyRevenue * inputs.servalaShare;
const actualServalaRevenue = month.servalaRevenue;
const bonusAmount = Math.max(0, standardRevenue - actualServalaRevenue); // CSP gets this as bonus
return sum + bonusAmount;
}, 0);
return totalPerformanceBonus;
});
this.charts.modelComparison.data.labels = comparisonLabels;
this.charts.modelComparison.data.datasets = [
{
label: `Loan Model (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed return)`,
data: loanModelData,
backgroundColor: '#ffc107',
borderColor: '#e0a800',
borderWidth: 2
});
}
},
{
label: 'Direct Investment (base return)',
data: directInvestmentData,
backgroundColor: enabledScenarios.map(scenario => colors[scenario] + '80'),
borderColor: enabledScenarios.map(scenario => colors[scenario]),
borderWidth: 2
},
{
label: 'Performance Bonus Impact',
data: performanceBonusData,
backgroundColor: enabledScenarios.map(scenario => colors[scenario] + '40'),
borderColor: enabledScenarios.map(scenario => colors[scenario]),
borderWidth: 2,
borderDash: [5, 5]
}
];
this.charts.modelComparison.update();
// Update Performance Comparison Chart (ROI comparison for both models)
this.charts.performance.data.labels = comparisonLabels;
// Calculate ROI for loan model (same for all scenarios)
const loanROI = (loanNetProfit / inputs.investmentAmount) * 100;
const loanROIData = enabledScenarios.map(() => loanROI);
// Get ROI for direct investment (varies by scenario)
const directROIData = enabledScenarios.map(scenario => {
const result = this.calculator.results[scenario];
return result ? result.roi : 0;
});
this.charts.performance.data.datasets = [
{
label: `Loan Model ROI (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed)`,
data: loanROIData,
backgroundColor: '#ffc107',
borderColor: '#e0a800',
borderWidth: 2
},
{
label: 'Direct Investment ROI',
data: directROIData,
backgroundColor: enabledScenarios.map(scenario => colors[scenario] + '80'),
borderColor: enabledScenarios.map(scenario => colors[scenario]),
borderWidth: 2
}
];
this.charts.performance.update();
} catch (error) {
console.error('Error updating charts:', error);
}
}
// Helper method to calculate loan payment
calculateLoanPayment(principal, annualRate, years) {
try {
const monthlyRate = annualRate / 12;
const numberOfPayments = years * 12;
if (monthlyRate === 0) {
return principal / numberOfPayments;
}
const monthlyPayment = principal * (monthlyRate * Math.pow(1 + monthlyRate, numberOfPayments)) /
(Math.pow(1 + monthlyRate, numberOfPayments) - 1);
return monthlyPayment;
} catch (error) {
console.error('Error calculating loan payment:', error);
return 0;
}
}
}