`;
companiesGrid.appendChild(card);
});
}
function updateSummaryMetrics(dataToSummarize) {
if (!overallSentimentLabelEl || !avgSentimentScoreEl || !positiveOutlookCountEl || !stableOutlookCountEl || !negativeOutlookCountEl) {
console.error("One or more summary metric DOM elements not found.");
return;
}
if (dataToSummarize.length === 0) {
overallSentimentLabelEl.textContent = 'N/A';
avgSentimentScoreEl.textContent = 'N/A';
positiveOutlookCountEl.textContent = '0';
stableOutlookCountEl.textContent = '0';
negativeOutlookCountEl.textContent = '0';
return;
}
let totalSentimentScore = 0;
let positiveOutlooks = 0;
let stableOutlooks = 0;
let negativeOutlooks = 0;
dataToSummarize.forEach(company => {
totalSentimentScore += company.marketSentimentScore;
// Count outlooks from S&P as primary, can be extended
const spOutlook = company.outlook.sp ? company.outlook.sp.toLowerCase() : '';
if (spOutlook === 'positive') positiveOutlooks++;
else if (spOutlook === 'stable') stableOutlooks++;
else if (spOutlook === 'negative') negativeOutlooks++;
});
const avgScore = totalSentimentScore / dataToSummarize.length;
avgSentimentScoreEl.textContent = `${avgScore.toFixed(1)}/100`;
if (avgScore >= 70) overallSentimentLabelEl.textContent = 'Positive';
else if (avgScore >= 50) overallSentimentLabelEl.textContent = 'Neutral';
else overallSentimentLabelEl.textContent = 'Negative';
overallSentimentLabelEl.className = `font-bold ${getSentimentTextClass(avgScore)}`;
positiveOutlookCountEl.textContent = positiveOutlooks;
stableOutlookCountEl.textContent = stableOutlooks;
negativeOutlookCountEl.textContent = negativeOutlooks;
}
function filterCompanies() {
if (!searchInput) {
console.error("Error: searchInput element not found.");
renderCompanies(companiesData); // Render all if search input is missing
updateSummaryMetrics(companiesData);
return;
}
const searchTerm = searchInput.value.toLowerCase().trim();
const filtered = companiesData.filter(company =>
company.name.toLowerCase().includes(searchTerm) ||
company.ticker.toLowerCase().includes(searchTerm)
);
renderCompanies(filtered);
updateSummaryMetrics(filtered); // Update summary based on filtered data
}
function generatePdf() {
if (!pdfContentEl) {
console.error("Error: pdfContent element not found. Cannot generate PDF.");
// You could show a user-friendly message here instead of an alert
const msgBox = document.createElement('div');
msgBox.textContent = "Error preparing PDF content. Please try again.";
msgBox.style.cssText = "position:fixed; top:20px; left:50%; transform:translateX(-50%); background:red; color:white; padding:10px; border-radius:5px; z-index:1000;";
document.body.appendChild(msgBox);
setTimeout(() => msgBox.remove(), 3000);
return;
}
const opt = {
margin: [0.5, 0.5, 0.5, 0.5], // inches [top, left, bottom, right]
filename: 'corporate_ratings_sentiment_dashboard.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, logging: false, useCORS: true, letterRendering: true, scrollY: 0 }, // Added scrollY:0
jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' },
pagebreak: { mode: ['avoid-all', 'css', 'legacy'] } // Helps with page breaks
};
// Temporarily make the hidden PDF title visible for html2pdf
const pdfTitleElement = pdfContentEl.querySelector('.print\\:block');
if (pdfTitleElement) {
pdfTitleElement.classList.remove('hidden');
}
// Create a clone of the content to be included in the PDF.
// This allows us to modify it for PDF without affecting the live view.
const contentCloneForPdf = pdfContentEl.cloneNode(true);
// Ensure company cards in the clone are styled for PDF (e.g., visible borders)
// This can be more robust by adding specific PDF print styles.
// For now, relying on @media print styles and html2pdf's rendering.
html2pdf().from(contentCloneForPdf).set(opt).save().then(() => {
// Re-hide the PDF title element after PDF generation
if (pdfTitleElement) {
pdfTitleElement.classList.add('hidden');
}
}).catch(err => {
console.error("Error generating PDF:", err);
// Re-hide the PDF title element in case of error too
if (pdfTitleElement) {
pdfTitleElement.classList.add('hidden');
}
const errorBox = document.createElement('div');
errorBox.textContent = "Failed to generate PDF. See console for details.";
errorBox.style.cssText = "position:fixed; top:20px; left:50%; transform:translateX(-50%); background:red; color:white; padding:10px; border-radius:5px; z-index:1000;";
document.body.appendChild(errorBox);
setTimeout(() => errorBox.remove(), 3000);
});
}
// --- Event Listeners ---
if (searchInput) {
searchInput.addEventListener('input', filterCompanies);
} else {
console.warn("Search input element not found. Search functionality will be disabled.");
}
if (downloadPdfButton) {
downloadPdfButton.addEventListener('click', generatePdf);
} else {
console.warn("PDF download button not found. PDF functionality will be disabled.");
}
// --- Initial Render ---
renderCompanies(companiesData);
updateSummaryMetrics(companiesData);
});