From ec64dd741558a250506ce08ae84e91517132e726 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Wed, 23 Jul 2025 15:45:40 +0200 Subject: [PATCH] add csp revenue chart --- hub/services/static/css/roi-calculator.css | 6 +- .../static/js/roi-calculator/chart-manager.js | 195 ++++++++++++++---- .../calculator/csp_roi_calculator.html | 8 +- 3 files changed, 166 insertions(+), 43 deletions(-) diff --git a/hub/services/static/css/roi-calculator.css b/hub/services/static/css/roi-calculator.css index a4525fb..2c1e4ac 100644 --- a/hub/services/static/css/roi-calculator.css +++ b/hub/services/static/css/roi-calculator.css @@ -124,7 +124,7 @@ } /* Secondary charts get good height */ -#revenueChart, #cashFlowChart, #modelComparisonChart { +#revenueChart, #cashFlowChart, #cspRevenueChart { height: 400px !important; } @@ -169,7 +169,7 @@ height: 350px !important; } - #revenueChart, #cashFlowChart, #modelComparisonChart { + #revenueChart, #cashFlowChart, #cspRevenueChart { height: 300px !important; } @@ -183,7 +183,7 @@ height: 250px !important; } - #revenueChart, #cashFlowChart, #modelComparisonChart { + #revenueChart, #cashFlowChart, #cspRevenueChart { height: 200px !important; } } diff --git a/hub/services/static/js/roi-calculator/chart-manager.js b/hub/services/static/js/roi-calculator/chart-manager.js index 5ebac63..d8e1657 100644 --- a/hub/services/static/js/roi-calculator/chart-manager.js +++ b/hub/services/static/js/roi-calculator/chart-manager.js @@ -83,30 +83,98 @@ class ChartManager { } }); - // Model Comparison Chart (replaces generic Cash Flow Chart) - const modelComparisonCanvas = document.getElementById('modelComparisonChart'); - if (!modelComparisonCanvas) { - console.error('Model comparison chart canvas not found'); + // CSP Revenue Breakdown Chart + const cspRevenueCanvas = document.getElementById('cspRevenueChart'); + if (!cspRevenueCanvas) { + console.error('CSP revenue breakdown chart canvas not found'); return; } - const modelComparisonCtx = modelComparisonCanvas.getContext('2d'); - this.charts.modelComparison = new Chart(modelComparisonCtx, { - type: 'bar', + const cspRevenueCtx = cspRevenueCanvas.getContext('2d'); + this.charts.cspRevenue = new Chart(cspRevenueCtx, { + type: 'line', data: { labels: [], datasets: [] }, options: { responsive: true, maintainAspectRatio: false, + layout: { + padding: { + left: 10, + right: 200, // Add space for side legend + top: 10, + bottom: 10 + } + }, plugins: { - legend: { position: 'top' }, - title: { display: true, text: 'Investment Model Comparison' } + legend: { + position: 'right', + align: 'start', + labels: { + boxWidth: 12, + padding: 15, + font: { + size: 11 + }, + usePointStyle: true, + generateLabels: function(chart) { + const original = Chart.defaults.plugins.legend.labels.generateLabels; + const labels = original.call(this, chart); + + // Group labels by scenario for better organization + return labels.map(label => { + // Shorten label text for better fit + if (label.text.includes('Service Revenue')) { + label.text = label.text.replace(' - Service Revenue', ' - Service'); + } + if (label.text.includes('Core Service Revenue')) { + label.text = label.text.replace(' - Core Service Revenue', ' - Core'); + } + if (label.text.includes('CSP Total')) { + label.text = label.text.replace(' - CSP Total', ' - Total'); + } + if (label.text.includes('Servala Revenue')) { + label.text = label.text.replace(' - Servala Revenue', ' - Servala'); + } + return label; + }); + } + } + }, + title: { + display: true, + text: 'CSP Revenue Growth - Direct Investment Only', + font: { + size: 14, + weight: 'bold' + } + } }, scales: { y: { beginAtZero: true, - title: { display: true, text: 'Total Return (CHF)' } + title: { display: true, text: 'Revenue Amount' }, + stacked: false, + grid: { + color: 'rgba(0,0,0,0.1)' + } }, x: { - title: { display: true, text: 'Growth Scenario' } + title: { display: true, text: 'Month' }, + grid: { + color: 'rgba(0,0,0,0.05)' + } + } + }, + interaction: { + mode: 'index', + intersect: false + }, + elements: { + point: { + radius: 3, + hoverRadius: 6 + }, + line: { + tension: 0.1 } } } @@ -148,7 +216,7 @@ class ChartManager { showChartError(message) { // Show error message in place of charts - const chartContainers = ['instanceGrowthChart', 'revenueChart', 'cashFlowChart', 'modelComparisonChart']; + const chartContainers = ['instanceGrowthChart', 'revenueChart', 'cashFlowChart', 'cspRevenueChart']; chartContainers.forEach(containerId => { const container = document.getElementById(containerId); if (container) { @@ -242,35 +310,90 @@ class ChartManager { const comparisonLabels = baseScenarios.map(s => this.calculator.scenarios[s].name); // Get net profit data for both models - const directInvestmentData = baseScenarios.map(scenario => { - const scenarioResult = this.calculator.results[scenario + '_direct']; - return scenarioResult ? scenarioResult.netPosition : 0; - }); - - const loanInvestmentData = baseScenarios.map(scenario => { - const scenarioResult = this.calculator.results[scenario + '_loan']; - return scenarioResult ? scenarioResult.netPosition : 0; - }); - - this.charts.modelComparison.data.labels = comparisonLabels; - this.charts.modelComparison.data.datasets = [ + // Update CSP Revenue Breakdown Chart (Direct Investment Only) + this.charts.cspRevenue.data.labels = monthLabels; + this.charts.cspRevenue.data.datasets = []; + + // Filter to only direct investment scenarios + const directScenarios = scenarios.filter(s => + this.calculator.results[s] && s.includes('_direct') + ); + + // Define revenue types and their styling + const revenueTypes = [ { - label: 'Direct Investment Model', - data: directInvestmentData, - backgroundColor: baseScenarios.map(scenario => colors[scenario] + '80'), - borderColor: baseScenarios.map(scenario => colors[scenario]), - borderWidth: 2 + key: 'serviceRevenue', + label: 'Service Revenue', + borderWidth: 2, + borderDash: [], + opacity: '40' }, { - label: `Loan Model (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed rate)`, - data: loanInvestmentData, - backgroundColor: '#ffc10780', - borderColor: '#e0a800', - borderWidth: 2 + key: 'coreRevenue', + label: 'Core Service Revenue', + borderWidth: 2, + borderDash: [3, 3], + opacity: '60' + }, + { + key: 'cspRevenue', + label: 'CSP Total', + borderWidth: 3, + borderDash: [], + opacity: 'FF' + }, + { + key: 'servalaRevenue', + label: 'Servala Revenue', + borderWidth: 1, + borderDash: [5, 5], + opacity: '80', + color: '#6c757d' } ]; - - this.charts.modelComparison.update(); + + // Add datasets organized by revenue type for better legend grouping + directScenarios.forEach(scenario => { + const scenarioBase = scenario.replace('_direct', ''); + const scenarioName = this.calculator.scenarios[scenarioBase]?.name || scenarioBase; + const monthlyData = this.calculator.monthlyData[scenario]; + const scenarioColor = colors[scenarioBase] || '#007bff'; + + revenueTypes.forEach(type => { + // Skip Servala revenue if it's zero (no revenue sharing) + if (type.key === 'servalaRevenue') { + const hasServalaRevenue = monthlyData.some(d => (d.servalaRevenue || 0) > 0); + if (!hasServalaRevenue) return; + } + + // Skip core revenue if it's zero + if (type.key === 'coreRevenue') { + const hasCoreRevenue = monthlyData.some(d => (d.coreRevenue || 0) > 0); + if (!hasCoreRevenue) return; + } + + const dataValues = monthlyData.map(d => { + if (type.key === 'serviceRevenue') return d.serviceRevenue || d.monthlyRevenue || 0; + return d[type.key] || 0; + }); + + this.charts.cspRevenue.data.datasets.push({ + label: `${scenarioName} - ${type.label}`, + data: dataValues, + borderColor: type.color || scenarioColor, + backgroundColor: (type.color || scenarioColor) + type.opacity, + borderWidth: type.borderWidth, + borderDash: type.borderDash, + fill: false, + tension: 0.1, + pointBackgroundColor: type.color || scenarioColor, + pointBorderColor: '#fff', + pointBorderWidth: 1 + }); + }); + }); + + this.charts.cspRevenue.update(); // Update Performance Comparison Chart (ROI comparison for both models) this.charts.performance.data.labels = comparisonLabels; diff --git a/hub/services/templates/calculator/csp_roi_calculator.html b/hub/services/templates/calculator/csp_roi_calculator.html index 740d456..c8e2180 100644 --- a/hub/services/templates/calculator/csp_roi_calculator.html +++ b/hub/services/templates/calculator/csp_roi_calculator.html @@ -584,16 +584,16 @@ document.addEventListener('DOMContentLoaded', function() { - +
-
Investment Model Comparison
-

Net profit comparison: Fixed loan returns vs. performance-based direct investment across scenarios

+
CSP Revenue Breakdown
+

Direct investment revenue breakdown: Service fees, core infrastructure sales, total CSP revenue, and Servala share over time

- +