further refinement of roi calculator
This commit is contained in:
parent
6f6c80480f
commit
493d45bb5d
6 changed files with 962 additions and 565 deletions
|
|
@ -101,14 +101,109 @@
|
|||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Enhanced chart sizing for new layout */
|
||||
.chart-container canvas {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
/* Full-width chart containers */
|
||||
.card-body canvas {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Primary chart gets extra height */
|
||||
#instanceGrowthChart {
|
||||
height: 500px !important;
|
||||
}
|
||||
|
||||
/* Secondary charts get good height */
|
||||
#revenueChart, #cashFlowChart, #modelComparisonChart {
|
||||
height: 400px !important;
|
||||
}
|
||||
|
||||
/* Enhanced layout styles for new design */
|
||||
.sticky-top {
|
||||
z-index: 1020;
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Compact header controls */
|
||||
.form-range {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.input-group-sm .form-control,
|
||||
.form-select-sm,
|
||||
.btn-sm {
|
||||
font-size: 0.825rem;
|
||||
}
|
||||
|
||||
/* Clean chart headers */
|
||||
.card-header {
|
||||
background: white !important;
|
||||
border: none !important;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Responsive chart heights */
|
||||
@media (max-width: 768px) {
|
||||
#instanceGrowthChart {
|
||||
height: 350px !important;
|
||||
}
|
||||
|
||||
#revenueChart, #cashFlowChart, #modelComparisonChart {
|
||||
height: 300px !important;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
#instanceGrowthChart {
|
||||
height: 250px !important;
|
||||
}
|
||||
|
||||
#revenueChart, #cashFlowChart, #modelComparisonChart {
|
||||
height: 200px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Manual collapse functionality */
|
||||
.collapse {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.collapsing {
|
||||
position: relative;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: height 0.35s ease;
|
||||
}
|
||||
|
||||
.export-buttons {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue