No risks defined to generate chart.
`;
return;
}
// Prepare Chart.js Scatter Plot Data
const riskData = metrics.riskLevels.map((level, i) => ({
x: metrics.riskNames[i], // Using name for x-axis to simulate categories or labels
y: metrics.riskScores[i], // Score for y-axis
likelihood: rmpo_data.items[i].likelihood,
impact: rmpo_data.items[i].impact,
name: metrics.riskNames[i],
level: level
}));
// Group data by level for datasets
const datasets = [];
Object.keys(RISK_COLOR_MAP).reverse().forEach(level => { // Reverse for better stacking in chart legend
const levelData = metrics.riskLevels.map((l, i) => l === level ? ({ x: metrics.riskNames[i], y: metrics.riskScores[i], likelihood: rmpo_data.items[i].likelihood, impact: rmpo_data.items[i].impact, name: metrics.riskNames[i] }) : null).filter(n => n);
if (levelData.length > 0) {
datasets.push({
label: level,
data: levelData.map(d => ({ x: d.likelihood, y: d.impact, name: d.name, score: d.likelihood * d.impact })), // Plot Likelihood (X) vs Impact (Y)
backgroundColor: RISK_COLOR_MAP[level],
pointRadius: 8,
pointStyle: 'circle'
});
}
});
// Convert names to indices for X-axis labeling
const uniqueLikelihoods = [...new Set(rmpo_data.items.map(item => item.likelihood))].sort((a, b) => a - b);
const uniqueImpacts = [...new Set(rmpo_data.items.map(item => item.impact))].sort((a, b) => a - b);
rmpo_chart = new Chart(rmpo_riskChartCanvas, {
type: 'scatter',
data: {
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
title: { display: true, text: 'Risk Matrix (Likelihood vs Impact)' },
tooltip: {
callbacks: {
label: (context) => {
const point = context.raw;
return `${point.name} (L: ${point.x}, I: ${point.y}, Score: ${point.score})`;
},
title: (context) => {
return context[0].dataset.label + ' Exposure';
}
}
}
},
scales: {
x: {
type: 'linear',
min: 0.5,
max: 5.5,
stepSize: 1,
title: { display: true, text: 'Likelihood (1-5)' },
ticks: { stepSize: 1, callback: function(value, index) { return value; } }
},
y: {
type: 'linear',
min: 0.5,
max: 5.5,
title: { display: true, text: 'Impact (1-5)' },
ticks: { stepSize: 1, callback: function(value, index) { return value; } }
}
}
}
});
}
function rmpo_renderDashboard() {
if (!rmpo_riskRegisterOutput) return;
// 1. Update Headers
rmpo_projectNameDisplay.textContent = rmpo_escapeHTML(rmpo_data.projectName) || "Untitled Project";
rmpo_projectContextDisplay.textContent = rmpo_escapeHTML(rmpo_data.projectContext) || "No context provided.";
const metrics = rmpo_calculateRiskMetrics();
// 2. Render Chart
rmpo_renderChart(metrics);
// 3. Render Metrics List
rmpo_metricsList.innerHTML = `
Total Risks Identified: ${metrics.totalRisks}
High Exposure: ${metrics.highCount}
Medium Exposure: ${metrics.mediumCount}
Low/Very Low Exposure: ${metrics.lowCount + metrics.veryLowCount}
`;
// 4. Render Register Table
let tableHTML = `
| Risk Name |
Score |
Level |
Owner |
Mitigation/Response Plan |
`;
if (rmpo_data.items.length === 0) {
tableHTML += `| No risks defined. |
`;
} else {
rmpo_data.items.forEach(item => {
const score = item.likelihood * item.impact;
const level = rmpo_calculateExposure(item.likelihood, item.impact);
const levelClass = rmpo_getMatrixClass(score);
tableHTML += `
| ${rmpo_escapeHTML(item.name)} |
${score} |
${level} |
${rmpo_escapeHTML(item.owner)} |
${rmpo_escapeHTML(item.response)} |
`;
});
}
tableHTML += `
`;
rmpo_riskRegisterOutput.innerHTML = tableHTML;
}
function rmpo_renderPdfClone() {
// Clone only the content structure for a cleaner PDF capture
rmpo_pdfRenderClone.innerHTML = document.getElementById('rmpo-dashboard-output').innerHTML;
const pdfChartCanvas = rmpo_pdfRenderClone.querySelector('#rmpo-risk-chart');
const chartContainer = rmpo_pdfRenderClone.querySelector('.chart-container');
// Temporarily set dimensions for Chart.js rendering on clone
if (chartContainer) {
chartContainer.style.height = '350px';
chartContainer.style.width = '350px';
}
// Temporarily render chart into PDF clone
const tempChart = new Chart(pdfChartCanvas, {
type: 'scatter',
data: rmpo_chart.data,
options: { ...rmpo_chart.options, animation: false, responsive: false, maintainAspectRatio: false } // Disable animation for capture
});
tempChart.update();
return tempChart;
}
/**
* Generates and downloads a PDF of the RMP
*/
async function rmpo_downloadPDF() {
if (rmpo_data.items.length === 0) {
alert("The risk register is empty. Please add items before downloading.");
return;
}
if (typeof jspdf === 'undefined' || typeof html2canvas === 'undefined') {
alert("Error: PDF libraries failed to load.");
return;
}
const tempChart = rmpo_renderPdfClone();
const { jsPDF } = window.jspdf;
try {
const canvas = await html2canvas(rmpo_pdfRenderClone, { scale: 1.5, useCORS: true });
const imgData = canvas.toDataURL('image/png');
const imgProps = { width: canvas.width, height: canvas.height };
const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' });
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const margin = 40;
const contentWidth = pdfWidth - (margin * 2);
const contentHeight = (contentWidth * imgProps.height) / imgProps.width;
let heightLeft = contentHeight;
let position = 0;
pdf.addImage(imgData, 'PNG', margin, position + margin, contentWidth, contentHeight);
heightLeft -= (pdfHeight - margin * 2);
while (heightLeft > 0) {
position -= (pdfHeight - margin * 2);
pdf.addPage();
pdf.addImage(imgData, 'PNG', margin, position + margin, contentWidth, contentHeight);
heightLeft -= (pdfHeight - margin * 2);
}
const safeName = (rmpo_data.projectName || 'risk_management_plan').replace(/[^a-z0-9]/gi, '_').toLowerCase();
pdf.save(`${safeName}_RMP.pdf`);
} catch (error) {
console.error("PDF generation failed:", error);
alert("An error occurred while generating the PDF.");
} finally {
tempChart.destroy(); // Clean up temporary chart instance
}
}
// --- EVENT LISTENERS ---
// Tab link clicks
rmpo_tabLinks.forEach((link, index) => {
link.addEventListener('click', () => rmpo_switchTab(index));
});
// Next/Prev button clicks
if (rmpo_prevButton) {
rmpo_prevButton.addEventListener('click', () => {
if (rmpo_currentTab > 0) rmpo_switchTab(rmpo_currentTab - 1);
});
}
if (rmpo_nextButton) {
rmpo_nextButton.addEventListener('click', () => {
if (rmpo_currentTab === rmpo_tabLinks.length - 1) {
rmpo_updateDataFromConfig();
rmpo_switchTab(0);
} else {
if (rmpo_currentTab < rmpo_tabLinks.length - 1) rmpo_switchTab(rmpo_currentTab + 1);
}
});
}
// PDF download
if (rmpo_downloadPdfButton) {
rmpo_downloadPdfButton.addEventListener('click', rmpo_downloadPDF);
}
// --- Config Tab Listeners ---
if (rmpo_addItemButton) {
rmpo_addItemButton.addEventListener('click', () => {
rmpo_itemsContainer.appendChild(rmpo_createItemInput());
});
}
if (rmpo_configTab) {
// Handle remove
rmpo_configTab.addEventListener('click', (e) => {
const removeButton = e.target.closest('.rmpo-remove-item');
if (removeButton) {
removeButton.closest('.border[data-id]').remove();
if(rmpo_itemsContainer.children.length === 0){
rmpo_itemsContainer.appendChild(rmpo_createItemInput());
}
}
});
}
// --- INITIALIZATION ---
rmpo_renderConfig();
rmpo_renderDashboard();
// Set initial tab state
rmpo_tabPanes.forEach((pane, index) => {
pane.classList.toggle('hidden', index !== 0);
pane.classList.toggle('rmpo-active', index === 0);
});
rmpo_tabLinks.forEach((link, index) => {
TAB_CLASSES.active.forEach(cls => link.classList.remove(cls));
TAB_CLASSES.inactive.forEach(cls => link.classList.remove(cls));
if (index === 0) {
TAB_CLASSES.active.forEach(cls => link.classList.add(cls));
} else {
TAB_CLASSES.inactive.forEach(cls => link.classList.add(cls));
}
});
});