Thematic Investing Planner

Thematic Investing Planner

Define Your Investment Themes and Their Portfolio Allocation

Enter each thematic area, its overall percentage of your total portfolio, and then break down its allocation into specific assets/ETFs. All percentages should sum to 100% within their respective scopes.

Please ensure all percentage allocations sum to 100% within their respective themes, and total portfolio theme allocations also sum to 100%.

Overall Portfolio Allocation Summary

Allocation by Investment Theme

Investment Theme Portfolio Allocation (%)
Total Portfolio Theme Allocation: 0.00%

Allocation by Individual Asset/ETF

Asset Name / Ticker Investment Theme Overall Portfolio Allocation (%)
Total Individual Asset Allocation: 0.00%

Define assets within this theme, and their share of this theme (should sum to 100%).

`; themesContainer.appendChild(themeGroup); themeGroup.querySelector('.remove-theme-button').addEventListener('click', function() { themeGroup.remove(); // Recalculate if on results tab if (activeTab === 'results') { calculateAllocations(); } }); themeGroup.querySelector('.add-asset-to-theme-button').addEventListener('click', function() { const currentThemeId = this.dataset.themeId; currentAssetIdCounter++; createAssetInputRow(currentThemeId, currentAssetIdCounter); }); } addThemeButton.addEventListener('click', function() { currentThemeId++; createThemeInputGroup(currentThemeId); }); // Add default example themes and assets createThemeInputGroup(++currentThemeId, 'Artificial Intelligence', '40'); createAssetInputRow(currentThemeId, ++currentAssetIdCounter, 'NVIDIA', '50'); createAssetInputRow(currentThemeId, ++currentAssetIdCounter, 'Microsoft', '30'); createAssetInputRow(currentThemeId, ++currentAssetIdCounter, 'ARKF ETF', '20'); createThemeInputGroup(++currentThemeId, 'Renewable Energy', '30'); createAssetInputRow(currentThemeId, ++currentAssetIdCounter, 'ENPH', '40'); createAssetInputRow(currentThemeId, ++currentAssetIdCounter, 'ICLN ETF', '60'); createThemeInputGroup(++currentThemeId, 'Digital Health', '30'); createAssetInputRow(currentThemeId, ++currentAssetIdCounter, 'TDOC', '35'); createAssetInputRow(currentThemeId, ++currentAssetIdCounter, 'LH', '35'); createAssetInputRow(currentThemeId, ++currentAssetIdCounter, 'Global X Telemedicine & Digital Health ETF', '30'); // --- Allocation Calculation Logic --- function calculateAllocations() { const themesData = []; let totalPortfolioThemeShare = 0; let overallAssetAllocationSum = 0; let validationErrors = []; const themeGroups = themesContainer.querySelectorAll('.theme-input-group'); if (themeGroups.length === 0) { errorMessage.style.display = 'block'; errorMessage.textContent = 'Please add at least one investment theme.'; themeAllocationTableBody.innerHTML = 'No themes defined.'; assetAllocationTableBody.innerHTML = 'No assets defined.'; totalThemeAllocationSpan.textContent = '0.00%'; totalAssetAllocationSpan.textContent = '0.00%'; return; } themeGroups.forEach(themeGroup => { const themeId = themeGroup.dataset.themeId; const themeName = document.getElementById(`themeNameInput${themeId}`).value.trim(); const themePortfolioShare = parseFloat(document.getElementById(`themePortfolioShare${themeId}`).value); if (!themeName) { validationErrors.push(`Theme name for Theme ${themeId} is missing.`); themeGroup.style.border = '1px solid red'; } else if (isNaN(themePortfolioShare) || themePortfolioShare < 0) { validationErrors.push(`Invalid 'Theme\'s Share of Total Portfolio' for "${themeName}".`); themeGroup.style.border = '1px solid red'; } else { themeGroup.style.border = '1px solid #e0e0e0'; // Reset border totalPortfolioThemeShare += themePortfolioShare; const assetsInTheme = []; let totalAssetShareInTheme = 0; const assetRows = themeGroup.querySelectorAll('.asset-row'); if (assetRows.length === 0) { validationErrors.push(`No assets defined for theme "${themeName}".`); } assetRows.forEach(assetRow => { const assetId = assetRow.dataset.assetId; const assetName = document.getElementById(`assetName${themeId}-${assetId}`).value.trim(); const assetShare = parseFloat(document.getElementById(`assetShare${themeId}-${assetId}`).value); if (!assetName) { validationErrors.push(`Asset name missing in theme "${themeName}".`); assetRow.style.border = '1px solid red'; } else if (isNaN(assetShare) || assetShare < 0) { validationErrors.push(`Invalid 'Share in Theme' for asset "${assetName}" in "${themeName}".`); assetRow.style.border = '1px solid red'; } else { assetRow.style.border = '1px solid #f9f9f9'; // Reset border assetsInTheme.push({ name: assetName, share: assetShare }); totalAssetShareInTheme += assetShare; } }); if (totalAssetShareInTheme > 0 && Math.abs(totalAssetShareInTheme - 100) > 0.01) { // Allow for small floating point errors validationErrors.push(`Asset shares in theme "${themeName}" do not sum to 100%. Current sum: ${totalAssetShareInTheme.toFixed(1)}%.`); themeGroup.style.border = '1px solid red'; } themesData.push({ name: themeName, portfolioShare: themePortfolioShare, assets: assetsInTheme, totalAssetShareInTheme: totalAssetShareInTheme // For validation }); } }); if (Math.abs(totalPortfolioThemeShare - 100) > 0.01) { validationErrors.push(`Total 'Theme\'s Share of Total Portfolio' does not sum to 100%. Current sum: ${totalPortfolioThemeShare.toFixed(1)}%.`); } if (validationErrors.length > 0) { errorMessage.style.display = 'block'; errorMessage.innerHTML = 'Please correct the following issues:
' + validationErrors.map(e => `• ${e}`).join('
'); // Clear results if there are errors themeAllocationTableBody.innerHTML = ''; assetAllocationTableBody.innerHTML = ''; totalThemeAllocationSpan.textContent = '0.00%'; totalAssetAllocationSpan.textContent = '0.00%'; return; } else { errorMessage.style.display = 'none'; } // Populate Theme Allocation Table themeAllocationTableBody.innerHTML = ''; themesData.forEach(theme => { const row = themeAllocationTableBody.insertRow(); row.innerHTML = ` ${theme.name} ${theme.portfolioShare.toFixed(1)}% `; }); totalThemeAllocationSpan.textContent = `${totalPortfolioThemeShare.toFixed(1)}%`; // Populate Asset Allocation Table assetAllocationTableBody.innerHTML = ''; overallAssetAllocationSum = 0; const assetSummary = {}; // To aggregate same assets across themes themesData.forEach(theme => { if (theme.portfolioShare > 0) { theme.assets.forEach(asset => { // Calculate overall portfolio allocation for each asset const assetOverallAllocation = (theme.portfolioShare / 100) * (asset.share / 100) * 100; // As percentage // Aggregate if asset appears in multiple themes if (assetSummary[asset.name]) { assetSummary[asset.name].allocation += assetOverallAllocation; } else { assetSummary[asset.name] = { theme: theme.name, // Store the first theme for display allocation: assetOverallAllocation }; } overallAssetAllocationSum += assetOverallAllocation; }); } }); // Convert aggregated asset summary to an array for display const sortedAssetNames = Object.keys(assetSummary).sort(); sortedAssetNames.forEach(assetName => { const data = assetSummary[assetName]; const row = assetAllocationTableBody.insertRow(); row.innerHTML = ` ${assetName} ${data.theme} ${data.allocation.toFixed(2)}% `; }); totalAssetAllocationSpan.textContent = `${overallAssetAllocationSum.toFixed(2)}%`; // Show results section document.getElementById('resultsTab').style.display = 'block'; } calculateResultsButton.addEventListener('click', calculateAllocations); // --- PDF Download Functionality --- downloadPdfButton.addEventListener('click', function() { const { jsPDF } = window.jspdf; const doc = new jsPDF(); doc.setFontSize(22); doc.text(toolTitle.textContent, doc.internal.pageSize.getWidth() / 2, 20, { align: 'center' }); let yPos = 40; // Section 1: Detailed Theme and Asset Inputs doc.setFontSize(16); doc.text("1. Defined Themes and Asset Allocations", 15, yPos); yPos += 10; const allInputData = []; const themeGroups = themesContainer.querySelectorAll('.theme-input-group'); themeGroups.forEach(themeGroup => { const themeId = themeGroup.dataset.themeId; const themeName = document.getElementById(`themeNameInput${themeId}`).value.trim(); const themePortfolioShare = parseFloat(document.getElementById(`themePortfolioShare${themeId}`).value); allInputData.push(['Theme:', themeName || 'N/A', '', '']); allInputData.push(['Theme Portfolio Share (%):', isNaN(themePortfolioShare) ? '' : themePortfolioShare.toFixed(1), '', '']); allInputData.push(['', '', '', '']); // Spacer row const assetRows = themeGroup.querySelectorAll('.asset-row'); if (assetRows.length > 0) { allInputData.push(['Assets within Theme:', '', '', '']); assetRows.forEach(assetRow => { const assetId = assetRow.dataset.assetId; const assetName = document.getElementById(`assetName${themeId}-${assetId}`).value.trim(); const assetShare = parseFloat(document.getElementById(`assetShare${themeId}-${assetId}`).value); allInputData.push(['', assetName || 'N/A', isNaN(assetShare) ? '' : `${assetShare.toFixed(1)}%`, '']); }); allInputData.push(['', '', '', '']); // Spacer } else { allInputData.push(['No assets defined for this theme.', '', '', '']); allInputData.push(['', '', '', '']); // Spacer } }); doc.autoTable({ startY: yPos, head: [['Category', 'Name/Share', 'Value', '']], body: allInputData, theme: 'grid', styles: { fontSize: 8, cellPadding: 2, overflow: 'linebreak' }, headStyles: { fillColor: [52, 152, 219], textColor: [255, 255, 255] }, alternateRowStyles: { fillColor: [236, 240, 241] }, margin: { top: 5, right: 15, bottom: 5, left: 15 }, columnStyles: { 0: { fontStyle: 'bold', cellWidth: 40 }, 1: { cellWidth: 60 }, 2: { cellWidth: 30, halign: 'right' }, 3: { cellWidth: 30 } } }); yPos = doc.autoTable.previous.finalY + 15; // Section 2: Allocation Summary doc.setFontSize(16); doc.text("2. Overall Portfolio Allocation Summary", 15, yPos); yPos += 10; doc.setFontSize(14); doc.text("Allocation by Investment Theme:", 15, yPos); yPos += 7; const themeAllocationHeaders = ['Investment Theme', 'Portfolio Allocation (%)']; const themeAllocationBody = []; const themeRows = themeAllocationTableBody.querySelectorAll('tr'); themeRows.forEach(row => { const cells = row.querySelectorAll('td'); themeAllocationBody.push([cells[0].textContent, cells[1].textContent]); }); doc.autoTable({ startY: yPos, head: [themeAllocationHeaders], body: themeAllocationBody, theme: 'grid', styles: { fontSize: 10, cellPadding: 3 }, headStyles: { fillColor: [52, 152, 219], textColor: [255, 255, 255] }, alternateRowStyles: { fillColor: [236, 240, 241] }, margin: { top: 5, right: 15, bottom: 5, left: 15 }, columnStyles: { 1: { halign: 'center', fontStyle: 'bold' } } }); yPos = doc.autoTable.previous.finalY + 10; doc.setFontSize(12); doc.text(`Total Portfolio Theme Allocation: ${totalThemeAllocationSpan.textContent}`, 15, yPos); yPos += 20; doc.setFontSize(14); doc.text("Allocation by Individual Asset/ETF:", 15, yPos); yPos += 7; const assetAllocationHeaders = ['Asset Name / Ticker', 'Investment Theme', 'Overall Portfolio Allocation (%)']; const assetAllocationBody = []; const assetRows = assetAllocationTableBody.querySelectorAll('tr'); assetRows.forEach(row => { const cells = row.querySelectorAll('td'); assetAllocationBody.push([cells[0].textContent, cells[1].textContent, cells[2].textContent]); }); doc.autoTable({ startY: yPos, head: [assetAllocationHeaders], body: assetAllocationBody, theme: 'grid', styles: { fontSize: 10, cellPadding: 3 }, headStyles: { fillColor: [52, 152, 219], textColor: [255, 255, 255] }, alternateRowStyles: { fillColor: [236, 240, 241] }, margin: { top: 5, right: 15, bottom: 5, left: 15 }, columnStyles: { 2: { halign: 'center', fontStyle: 'bold' } } }); yPos = doc.autoTable.previous.finalY + 10; doc.setFontSize(12); doc.text(`Total Individual Asset Allocation: ${totalAssetAllocationSpan.textContent}`, 15, yPos); // Add page numbers let totalPages = doc.internal.getNumberOfPages(); for (let i = 1; i <= totalPages; i++) { doc.setPage(i); doc.setFontSize(10); doc.text("Page " + i + " of " + totalPages, doc.internal.pageSize.getWidth() / 2, doc.internal.pageSize.height - 10, { align: 'center' }); } doc.save('Thematic_Investing_Planner.pdf'); }); // Initial calculation on load with default values calculateAllocations(); });
Scroll to Top