Earned Value Dashboard
Project Performance Overview
Track your project's progress, schedule, and cost performance using Earned Value Management (EVM).
Planned Value (PV)
$0.00
Earned Value (EV)
$0.00
Actual Cost (AC)
$0.00
Schedule Variance (SV)
$0.00
Cost Variance (CV)
$0.00
Schedule Performance Index (SPI)
N/A
Cost Performance Index (CPI)
N/A
EVM Trend (PV, EV, AC)
SPI & CPI Performance
Configure Project Tasks
Add, edit, or remove individual tasks for your project.
Add/Edit Task
| ID | Task Name | Date | PV ($) | EV ($) | AC ($) | Status | Actions |
|---|
No task data available.
'; return; } // Calculate overall SPI and CPI const totalPV = projectTasks.reduce((sum, t) => sum + t.plannedValue, 0); const totalEV = projectTasks.reduce((sum, t) => sum + t.earnedValue, 0); const totalAC = projectTasks.reduce((sum, t) => sum + t.actualCost, 0); const spi = totalPV > 0 ? (totalEV / totalPV) : 0; const cpi = totalAC > 0 ? (totalEV / totalAC) : 0; const chartData = [ { metric: 'SPI', value: spi }, { metric: 'CPI', value: cpi } ]; const margin = { top: 20, right: 20, bottom: 40, left: 40 }; const width = spiCpiChartContainer.clientWidth - margin.left - margin.right; const height = spiCpiChartContainer.clientHeight - margin.top - margin.bottom - 30; // Account for title height const svg = d3.select(spiCpiChartContainer) .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.metric)) .range([0, width]) .padding(0.3); // Y scale (from 0 to max(1.2, max_value)) const maxY = Math.max(1.2, d3.max(chartData, d => d.value) * 1.1); const y = d3.scaleLinear() .domain([0, maxY]) .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(".2f"))); // Format to 2 decimal places // Add reference line at Y=1.0 svg.append("line") .attr("x1", 0) .attr("y1", y(1.0)) .attr("x2", width) .attr("y2", y(1.0)) .attr("stroke", "#999") .attr("stroke-dasharray", "4 4") .attr("stroke-width", 1); svg.append("text") .attr("x", width + 5) .attr("y", y(1.0)) .attr("dy", "0.32em") .attr("text-anchor", "start") .style("font-size", "10px") .style("fill", "#666") .text("Target (1.0)"); // 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.metric)) .attr("y", d => y(d.value)) .attr("width", x.bandwidth()) .attr("height", d => height - y(d.value)) .attr("fill", d => { if (d.value >= 1) return '#28a745'; // Green (on or above target) if (d.value > 0) return '#dc3545'; // Red (below target, but positive) return '#6c757d'; // Gray (zero or N/A) }) .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(`Metric: ${d.metric}Value: ${d.value.toFixed(2)}`) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 28) + "px"); }) .on("mouseout", function(d) { d3.select(this).attr("fill", d => { if (d.value >= 1) return '#28a745'; if (d.value > 0) return '#dc3545'; return '#6c757d'; }); // 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 EVM trend chart const evmChartCanvas = await html2canvas(evmTrendChartContainer, { scale: 2, useCORS: true, logging: false }); const evmChartImgData = evmChartCanvas.toDataURL('image/png'); // Capture SPI/CPI chart const spiCpiChartCanvas = await html2canvas(spiCpiChartContainer, { scale: 2, useCORS: true, logging: false }); const spiCpiChartImgData = spiCpiChartCanvas.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("Earned Value Management 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 EVM 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 EVM Trend Chart Image pdf.setFontSize(18); pdf.text("EVM Trend (PV, EV, AC) Chart", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 10; const evmChartImgHeight = (evmChartCanvas.height * (pdfWidth - 40)) / evmChartCanvas.width; // Check if image fits on current page, add new page if not if (yOffset + evmChartImgHeight > pdf.internal.pageSize.getHeight() - 20) { pdf.addPage(); yOffset = 40; // Reset yOffset for new page } pdf.addImage(evmChartImgData, 'PNG', 20, yOffset, pdfWidth - 40, evmChartImgHeight); yOffset += evmChartImgHeight + 30; // Add SPI & CPI Chart Image pdf.setFontSize(18); pdf.text("SPI & CPI Performance Chart", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 10; const spiCpiChartImgHeight = (spiCpiChartCanvas.height * (pdfWidth - 40)) / spiCpiChartCanvas.width; // Check if image fits on current page, add new page if not if (yOffset + spiCpiChartImgHeight > pdf.internal.pageSize.getHeight() - 20) { pdf.addPage(); yOffset = 40; // Reset yOffset for new page } pdf.addImage(spiCpiChartImgData, 'PNG', 20, yOffset, pdfWidth - 40, spiCpiChartImgHeight); yOffset += spiCpiChartImgHeight + 30; // Add a section for detailed task data (tabular format) pdf.addPage(); pdf.setFontSize(18); pdf.text("Detailed Task Information", pdf.internal.pageSize.getWidth() / 2, 40, { align: 'center' }); const tableData = projectTasks.map(t => [ t.id, t.taskName, t.reportingDate, `$${t.plannedValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, `$${t.earnedValue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, `$${t.actualCost.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, t.taskStatus, t.notes || 'N/A' ]); pdf.autoTable({ head: [['ID', 'Task Name', 'Date', 'PV ($)', 'EV ($)', 'AC ($)', 'Status', 'Notes']], body: tableData, startY: 60, theme: 'grid', styles: { fontSize: 7, cellPadding: 3 }, // Smaller font for more columns headStyles: { fillColor: [242, 242, 242], textColor: [51, 51, 51], fontStyle: 'bold' }, alternateRowStyles: { fillColor: [251, 251, 251] }, margin: { top: 70, left: 5, right: 5 } // Adjust margins for more columns }); pdf.save('earned_value_report.pdf'); }; // --- Event Listeners and Initial Render --- downloadPdfButton.addEventListener('click', downloadPdf); // Initial setup // Sort tasks by reporting date initially projectTasks.sort((a, b) => new Date(a.reportingDate) - new Date(b.reportingDate)); updateConfigTable(); renderDashboardMetrics(); renderEVMTrendChart(); renderSPICPIChart(); updateNavigationButtons(); // Set initial button states });
