Diversification Strategy Dashboard
Diversification Overview
Analyze your asset allocation and risk profile.
Total Portfolio Value ($)
$0.00
Number of Assets
0
Average Risk Level
N/A
Your Assets
Diversification by Asset Type
Diversification by Risk Level
Configure Your Assets
Add, edit, or remove individual assets for your portfolio.
Add/Edit Asset
| ID | Name | Type | Value ($) | Risk Level | Actions |
|---|
No assets configured. Go to "Asset Configuration" to add some.
'; return; } assets.forEach(a => { const card = document.createElement('div'); card.classList.add('asset-card'); let riskClass = ''; switch (a.riskLevel) { case 'Low': riskClass = 'risk-low'; break; case 'Medium': riskClass = 'risk-medium'; break; case 'High': riskClass = 'risk-high'; break; default: riskClass = 'risk-low'; } card.innerHTML = `${a.name}
Type: ${a.type}
Value: $${a.value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
Notes: ${a.notes || 'N/A'}
${a.riskLevel} Risk ${a.type} `; assetCardsContainer.appendChild(card); }); } /** * Renders a pie chart of diversification by asset type using D3.js. */ function renderDiversificationByTypeChart() { if (!diversificationByTypeChartContainer) { console.error('Error: diversificationByTypeChartContainer not found.'); return; } // Clear previous chart, but keep the title div const chartSvg = diversificationByTypeChartContainer.querySelector('svg'); if (chartSvg) { chartSvg.remove(); } if (assets.length === 0) { diversificationByTypeChartContainer.innerHTML = 'Diversification by Asset Type
No asset data available.
'; return; } // Aggregate data by asset type const typeValues = d3.rollup(assets, v => d3.sum(v, d => d.value), d => d.type); const chartData = Array.from(typeValues, ([type, value]) => ({ type, value })); const width = diversificationByTypeChartContainer.clientWidth; const height = diversificationByTypeChartContainer.clientHeight - 30; // Account for title height const radius = Math.min(width, height) / 2 - 20; // Adjusted radius const svg = d3.select(diversificationByTypeChartContainer) .append("svg") .attr("width", width) .attr("height", height) .append("g") .attr("transform", `translate(${width / 2},${height / 2})`); const pie = d3.pie() .sort(null) .value(d => d.value); const arc = d3.arc() .innerRadius(0) .outerRadius(radius); const color = d3.scaleOrdinal(d3.schemeCategory10); // D3's default color scheme // Tooltip const tooltip = d3.select("body").append("div") .attr("class", "chart-tooltip") .style("opacity", 0); const arcs = svg.selectAll(".arc") .data(pie(chartData)) .enter().append("g") .attr("class", "arc"); arcs.append("path") .attr("d", arc) .attr("fill", d => color(d.data.type)) .on("mouseover", function(event, d) { d3.select(this).attr("fill", d3.rgb(color(d.data.type)).darker(0.5)); tooltip.transition() .duration(200) .style("opacity", .9); tooltip.html(`Type: ${d.data.type}Value: $${d.data.value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
Percentage: ${(d.data.value / d3.sum(chartData, d => d.value) * 100).toFixed(1)}%`) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 28) + "px"); }) .on("mouseout", function(d) { d3.select(this).attr("fill", color(d.data.type)); // Restore original color tooltip.transition() .duration(500) .style("opacity", 0); }); // Add text labels arcs.append("text") .attr("transform", d => `translate(${arc.centroid(d)})`) .attr("dy", "0.35em") .text(d => `${d.data.type} (${(d.data.value / d3.sum(chartData, d => d.value) * 100).toFixed(0)}%)`) .style("text-anchor", "middle") .style("font-size", "10px") .style("fill", "white") .style("pointer-events", "none"); // Prevent text from blocking mouse events on arcs } /** * Renders a bar chart of diversification by risk level using D3.js. */ function renderDiversificationByRiskChart() { if (!diversificationByRiskChartContainer) { console.error('Error: diversificationByRiskChartContainer not found.'); return; } // Clear previous chart, but keep the title div const chartSvg = diversificationByRiskChartContainer.querySelector('svg'); if (chartSvg) { chartSvg.remove(); } if (assets.length === 0) { diversificationByRiskChartContainer.innerHTML = '
Diversification by Risk Level
No asset data available.
'; return; } // Aggregate data by risk level const riskCounts = d3.rollup(assets, v => v.length, d => d.riskLevel); const chartData = Array.from(riskCounts, ([riskLevel, count]) => ({ riskLevel, count })); // Define a consistent order and color for risk levels const riskOrder = ['Low', 'Medium', 'High']; const riskColors = { 'Low': '#28a745', // Green 'Medium': '#ffc107', // Amber 'High': '#dc3545' // Red }; chartData.sort((a, b) => riskOrder.indexOf(a.riskLevel) - riskOrder.indexOf(b.riskLevel)); const margin = { top: 20, right: 20, bottom: 40, left: 40 }; const width = diversificationByRiskChartContainer.clientWidth - margin.left - margin.right; const height = diversificationByRiskChartContainer.clientHeight - margin.top - margin.bottom - 30; // Account for title height const svg = d3.select(diversificationByRiskChartContainer) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); // X scale const x = d3.scaleBand() .domain(chartData.map(d => d.riskLevel)) .range([0, width]) .padding(0.1); // Y scale const y = d3.scaleLinear() .domain([0, d3.max(chartData, d => d.count) * 1.2]) // 20% buffer .range([height, 0]); // Add X axis svg.append("g") .attr("class", "axis x-axis") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x)); // Add Y axis svg.append("g") .attr("class", "axis y-axis") .call(d3.axisLeft(y).tickFormat(d3.format("d"))); // Integer ticks // Tooltip const tooltip = d3.select("body").append("div") .attr("class", "chart-tooltip") .style("opacity", 0); // Add bars svg.selectAll(".bar") .data(chartData) .enter().append("rect") .attr("class", "bar") .attr("x", d => x(d.riskLevel)) .attr("y", d => y(d.count)) .attr("width", x.bandwidth()) .attr("height", d => height - y(d.count)) .attr("fill", d => riskColors[d.riskLevel] || 'steelblue') // Use defined colors .on("mouseover", function(event, d) { d3.select(this).attr("fill", d3.rgb(this.getAttribute('fill')).darker(0.5)); tooltip.transition() .duration(200) .style("opacity", .9); tooltip.html(`Risk Level: ${d.riskLevel}Assets: ${d.count}`) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 28) + "px"); }) .on("mouseout", function(d) { d3.select(this).attr("fill", riskColors[d.riskLevel] || 'steelblue'); // Restore original color tooltip.transition() .duration(500) .style("opacity", 0); }); } // --- PDF Download Function --- /** * Generates and downloads a PDF of the current dashboard content. */ window.downloadPdf = async function() { if (!dashboardTab) { console.error('Error: Dashboard tab content not found for PDF generation.'); return; } // Temporarily hide elements not needed in PDF const elementsToHide = document.querySelectorAll('.tab-nav, .nav-buttons, #downloadPdfButton, .chart-tooltip'); elementsToHide.forEach(el => el.style.display = 'none'); // Ensure the dashboard tab is active for capture dashboardTab.classList.add('active'); // Capture summary metrics const summaryCanvas = await html2canvas(document.querySelector('.summary-metrics'), { scale: 2, useCORS: true, logging: false }); const summaryImgData = summaryCanvas.toDataURL('image/png'); // Capture asset cards const cardsCanvas = await html2canvas(assetCardsContainer, { scale: 2, useCORS: true, logging: false }); const cardsImgData = cardsCanvas.toDataURL('image/png'); // Capture diversification by type chart const typeChartCanvas = await html2canvas(diversificationByTypeChartContainer, { scale: 2, useCORS: true, logging: false }); const typeChartImgData = typeChartCanvas.toDataURL('image/png'); // Capture diversification by risk chart const riskChartCanvas = await html2canvas(diversificationByRiskChartContainer, { scale: 2, useCORS: true, logging: false }); const riskChartImgData = riskChartCanvas.toDataURL('image/png'); // Re-show hidden elements immediately after capture elementsToHide.forEach(el => el.style.display = ''); const { jsPDF } = window.jspdf; const pdf = new jsPDF({ orientation: 'portrait', unit: 'px', format: 'a4' }); const pdfWidth = pdf.internal.pageSize.getWidth(); let yOffset = 40; // Add title to PDF pdf.setFontSize(22); pdf.text("Diversification Strategy Report", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 20; // Add current date/time to PDF pdf.setFontSize(10); pdf.text(`Generated on: ${new Date().toLocaleString()}`, pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 40; // Add Summary Metrics Image pdf.setFontSize(18); pdf.text("Summary Metrics", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 10; const summaryImgHeight = (summaryCanvas.height * (pdfWidth - 40)) / summaryCanvas.width; pdf.addImage(summaryImgData, 'PNG', 20, yOffset, pdfWidth - 40, summaryImgHeight); yOffset += summaryImgHeight + 30; // Add Asset Cards Image pdf.setFontSize(18); pdf.text("Your Assets Overview", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 10; const cardsImgHeight = (cardsCanvas.height * (pdfWidth - 40)) / cardsCanvas.width; // Check if image fits on current page, add new page if not if (yOffset + cardsImgHeight > pdf.internal.pageSize.getHeight() - 20) { pdf.addPage(); yOffset = 40; // Reset yOffset for new page } pdf.addImage(cardsImgData, 'PNG', 20, yOffset, pdfWidth - 40, cardsImgHeight); yOffset += cardsImgHeight + 30; // Add Diversification by Type Chart Image pdf.setFontSize(18); pdf.text("Diversification by Asset Type Chart", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 10; const typeChartImgHeight = (typeChartCanvas.height * (pdfWidth - 40)) / typeChartCanvas.width; // Check if image fits on current page, add new page if not if (yOffset + typeChartImgHeight > pdf.internal.pageSize.getHeight() - 20) { pdf.addPage(); yOffset = 40; // Reset yOffset for new page } pdf.addImage(typeChartImgData, 'PNG', 20, yOffset, pdfWidth - 40, typeChartImgHeight); yOffset += typeChartImgHeight + 30; // Add Diversification by Risk Chart Image pdf.setFontSize(18); pdf.text("Diversification by Risk Level Chart", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 10; const riskChartImgHeight = (riskChartCanvas.height * (pdfWidth - 40)) / riskChartCanvas.width; // Check if image fits on current page, add new page if not if (yOffset + riskChartImgHeight > pdf.internal.pageSize.getHeight() - 20) { pdf.addPage(); yOffset = 40; // Reset yOffset for new page } pdf.addImage(riskChartImgData, 'PNG', 20, yOffset, pdfWidth - 40, riskChartImgHeight); yOffset += riskChartImgHeight + 30; // Add a section for detailed asset data (tabular format) pdf.addPage(); pdf.setFontSize(18); pdf.text("Detailed Asset Information", pdf.internal.pageSize.getWidth() / 2, 40, { align: 'center' }); const tableData = assets.map(a => [ a.id, a.name, a.type, `$${a.value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, a.riskLevel, a.notes || 'N/A' ]); pdf.autoTable({ head: [['ID', 'Name', 'Type', 'Value ($)', 'Risk Level', 'Notes']], body: tableData, startY: 60, theme: 'grid', styles: { fontSize: 8, cellPadding: 4 }, headStyles: { fillColor: [242, 242, 242], textColor: [51, 51, 51], fontStyle: 'bold' }, alternateRowStyles: { fillColor: [251, 251, 251] }, margin: { top: 70, left: 10, right: 10 } // Adjust margins for more columns }); pdf.save('diversification_strategy_report.pdf'); }; // --- Event Listeners and Initial Render --- downloadPdfButton.addEventListener('click', downloadPdf); // Initial setup (no specific sort order needed for assets initially) updateConfigTable(); renderDashboardMetrics(); renderAssetCards(); renderDiversificationByTypeChart(); renderDiversificationByRiskChart(); updateNavigationButtons(); // Set initial button states });
