document.addEventListener("DOMContentLoaded", () => { const toggle = document.getElementById("exportToggle"); const items = document.getElementById("exportItems"); const close = document.getElementById("exportClose"); if (!toggle || !items) return; toggle.addEventListener("click", () => { items.classList.toggle("show"); toggle.innerText = items.classList.contains("show") ? "Export ▾" : "Export ▸"; }); if (close) { close.addEventListener("click", () => { items.classList.remove("show"); toggle.innerText = "Export ▸"; }); } }); function openChartSelector() { if (typeof charts === "undefined" || Object.keys(charts).length === 0) { alert("Charts are still loading. Please try again."); return; } const modal = document.getElementById("chartModal"); const list = document.getElementById("chartList"); list.innerHTML = ""; Object.keys(charts).forEach(id => { list.innerHTML += ` `; }); list.innerHTML += ` `; modal.style.display = "block"; } function closeChartSelector() { document.getElementById("chartModal").style.display = "none"; } function formatChartName(id) { const map = { trendChart: "Case Trend", pathogenChart: "Pathogen Distribution", provinceMap: "Geographic Distribution", ageChart: "Age Distribution", sexChart: "Sex Distribution", subtypeChart: "Influenza Subtypes", sentinelChart: "Sentinel Site Distribution", sequencingTotalChart: "Sequencing Total", covidLineageFrequency: "Covid Lineage Frequency", influenzaSubtypeDistribution: "Influenza Subtype Frequency", covidDistributedByAgeGroup: "Covid Cases by Age", influenzaSubtypeFrequency: "Influenza Cases by Age", }; return map[id] || id; } async function exportSelectedCharts() { const { jsPDF } = window.jspdf; const pdf = new jsPDF("l", "mm", "a4"); const margin = 15; const pageWidth = 297; const pageHeight = 210; const contentTop = 35; const contentHeight = pageHeight - contentTop - 10; const contentWidth = pageWidth - margin * 2; const selected = document.querySelectorAll("#chartList input:checked"); const loadingDiv = document.createElement("div"); Object.assign(loadingDiv.style, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", background: "rgb(255, 255, 255)", color: "#fff", padding: "20px", borderRadius: "10px", zIndex: "10000" }); loadingDiv.innerHTML = "Generating PDF..."; document.body.appendChild(loadingDiv); function drawHeader() { pdf.setFillColor(255, 255, 255); pdf.rect(0, 0, pageWidth, 30, "F"); pdf.setFontSize(18); pdf.setFont(undefined, "bold"); pdf.setTextColor(0); pdf.text(getReportTitle(), margin, 14); pdf.setFontSize(9); pdf.setTextColor(100); pdf.text(`Generated: ${new Date().toLocaleString()}`, margin, 22); pdf.setDrawColor(220); pdf.line(margin, 28, pageWidth - margin, 28); } drawHeader(); const items = []; for (const cb of selected) { if (cb.value === "provinceMap") { loadingDiv.innerHTML = "Processing map..."; const img = await getMapImage(); if (img) { items.push({ id: "provinceMap", type: "map", img }); } } else { const chart = charts[cb.value]; if (!chart) continue; items.push({ id: cb.value, type: "chart", chart }); } } for (let i = 0; i < items.length; i++) { const item = items[i]; loadingDiv.innerHTML = `Rendering ${formatChartName(item.id)} (${i + 1}/${items.length})`; if (i !== 0) { pdf.addPage(); drawHeader(); } const cardX = margin; const cardY = contentTop - 5; const cardWidth = contentWidth; const cardHeight = contentHeight; pdf.setFillColor(255, 255, 255); pdf.roundedRect(cardX, cardY, cardWidth, cardHeight, 3, 3, "F"); pdf.setDrawColor(230); pdf.roundedRect(cardX, cardY, cardWidth, cardHeight, 3, 3); pdf.setFontSize(13); pdf.setFont(undefined, "bold"); pdf.setTextColor(0); pdf.text(formatChartName(item.id), cardX + 5, cardY + 8); let img; let width; let height; if (item.type === "map") { img = item.img; width = cardWidth - 50; height = width * 0.65; } else { const canvas = item.chart.canvas; const scale = 4; const tempCanvas = document.createElement("canvas"); tempCanvas.width = canvas.width * scale; tempCanvas.height = canvas.height * scale; const ctx = tempCanvas.getContext("2d"); ctx.scale(scale, scale); ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(canvas, 0, 0); img = tempCanvas.toDataURL("image/jpeg", 0.95); const ratio = canvas.height / canvas.width; width = cardWidth - 10; height = width * ratio; if (height > cardHeight - 20) { height = cardHeight - 20; width = height / ratio; } } const x = cardX + (cardWidth - width) / 2; const contentTopOffset = 14; const availableHeight = cardHeight - contentTopOffset; const y = cardY + contentTopOffset + (availableHeight - height) / 2; pdf.addImage(img, "PNG", x, y, width, height); } document.body.removeChild(loadingDiv); pdf.save("dashboard_report.pdf"); closeChartSelector(); } function prepareMapForExport() { if (!window.map) return; const bounds = L.latLngBounds([ [10.3, 102.3], [14.7, 107.6] ]); window.map.fitBounds(bounds); } async function exportFullDashboard() { const el = document.querySelector(".content-area"); const mapEl = document.getElementById("provinceMap"); let originalMapHTML = null; if (mapEl) { originalMapHTML = mapEl.innerHTML; const mapImg = await getMapImage(); if (mapImg) { mapEl.innerHTML = ``; } } prepareMapForExport(); await new Promise(resolve => setTimeout(resolve, 800)); const canvas = await html2canvas(el, { scale: 4, useCORS: true, scrollY: -window.scrollY, backgroundColor: "#ffffff" }); if (mapEl && originalMapHTML !== null) { mapEl.innerHTML = originalMapHTML; window.map.invalidateSize(); } const img = canvas.toDataURL("image/jpeg", 0.95); const { jsPDF } = window.jspdf; const pdf = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4", compress: true }); const pageWidth = 297; const pageHeight = 210; const ratio = Math.min( pageWidth / canvas.width, pageHeight / canvas.height ); const imgWidth = canvas.width * ratio; const imgHeight = canvas.height * ratio; const x = (pageWidth - imgWidth) / 2; const y = (pageHeight - imgHeight) / 2; pdf.addImage(img, "JPEG", x, y, imgWidth, imgHeight); pdf.save("full_dashboard.pdf"); closeChartSelector(); } function getColor(value) { if (value > 50) return "#b91c1c"; if (value >= 10) return "#ef4444"; if (value > 0) return "#fecaca"; return "#f3f4f600"; } async function getMapImage() { const original = document.getElementById("provinceMap"); if (!original) return null; const width = original.offsetWidth; const height = original.offsetHeight; const canvas = document.createElement("canvas"); canvas.width = width * 3; canvas.height = height * 3; const ctx = canvas.getContext("2d"); ctx.scale(3, 3); ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, width, height); const zoom = window.map.getZoom(); const projection = window.map.options.crs; const projectedRings = []; let minX = Infinity, minY = Infinity; let maxX = -Infinity, maxY = -Infinity; const totals = {}; const rows = window.latestProvinceData || []; rows.forEach(r => { const province = window.normalizeProvince(r.patient_province, window.validProvinces); if (!province) return; if (!totals[province]) { totals[province] = { total: 0, positive: 0 }; } totals[province].total += Number(r.total); totals[province].positive += Number(r.positive); }); function getColor(value) { if (value > 50) return "#b91c1c"; if (value >= 10) return "#ef4444"; if (value > 0) return "#fecaca"; return "#f3f4f600"; } window.map.eachLayer(layer => { if (!layer.toGeoJSON) return; if (layer instanceof L.CircleMarker) { const latlng = layer.getLatLng(); const point = projection.latLngToPoint(latlng, zoom); minX = Math.min(minX, point.x); maxX = Math.max(maxX, point.x); minY = Math.min(minY, point.y); maxY = Math.max(maxY, point.y); projectedRings.push({ type: "circle", x: point.x, y: point.y, radius: layer.getRadius(), fillColor: layer.options.fillColor || "#000", strokeColor: layer.options.color || "#000" }); return; } const geo = layer.toGeoJSON(); const features = geo.type === "FeatureCollection" ? geo.features : [geo]; features.forEach(f => { if ( !f.geometry || !f.geometry.coordinates ) return; const coords = f.geometry.coordinates; const polygons = f.geometry.type === "MultiPolygon" ? coords : [coords]; polygons.forEach(poly => { if (!Array.isArray(poly)) return; poly.forEach(ring => { if ( !Array.isArray(ring) || !Array.isArray(ring[0]) ) { return; } const projected = ring.map(coord => { if (!Array.isArray(coord) || coord.length < 2) { return null; } const [lng, lat] = coord; const latlng = L.latLng(lat, lng); const point = projection.latLngToPoint(latlng, zoom); minX = Math.min(minX, point.x); maxX = Math.max(maxX, point.x); minY = Math.min(minY, point.y); maxY = Math.max(maxY, point.y); return [point.x, point.y]; }).filter(Boolean); if (!projected.length) return; projectedRings.push({ points: projected, properties: f.properties || {} }); }); }); }); }); const padding = 10; const scaleX = (width - padding * 2) / (maxX - minX); const scaleY = (height - padding * 2) / (maxY - minY); const scale = Math.min(scaleX, scaleY); const offsetX = (width - (maxX - minX) * scale) / 2; const offsetY = (height - (maxY - minY) * scale) / 2; projectedRings.forEach(item => { // ------------------------- // Draw circle markers // ------------------------- if (item.type === "circle") { const drawX = (item.x - minX) * scale + offsetX; const drawY = (item.y - minY) * scale + offsetY; ctx.beginPath(); ctx.arc( drawX, drawY, item.radius, 0, Math.PI * 2 ); ctx.fillStyle = item.fillColor; ctx.fill(); ctx.strokeStyle = item.strokeColor; ctx.lineWidth = 2; ctx.stroke(); return; } // ------------------------- // Draw polygons // ------------------------- const { points, properties } = item; ctx.beginPath(); points.forEach(([x, y], i) => { const drawX = (x - minX) * scale + offsetX; const drawY = (y - minY) * scale + offsetY; if (i === 0) ctx.moveTo(drawX, drawY); else ctx.lineTo(drawX, drawY); }); ctx.closePath(); const province = properties.ADM1_EN; const value = totals[province]?.total || 0; ctx.fillStyle = getColor(value); ctx.fill(); ctx.strokeStyle = "#444"; ctx.lineWidth = 1; ctx.stroke(); }); const legend = document.querySelector(".map-legend"); if (legend) { const legendCanvas = await html2canvas(legend, { scale: 3, backgroundColor: null }); const lw = legend.offsetWidth; const lh = legend.offsetHeight; ctx.drawImage( legendCanvas, width - lw - 10, height - lh - 10, lw, lh ); } return canvas.toDataURL("image/png"); } function getReportTitle() { const program = window.PROGRAM_CODE || "Overview"; const rangeType = document.getElementById("trend_range")?.value; if (rangeType === "custom") { const sy = document.getElementById("start_year")?.value; const sw = document.getElementById("start_week")?.value; const ey = document.getElementById("end_year")?.value; const ew = document.getElementById("end_week")?.value; return `${program} Report (W${sw}/${sy} – W${ew}/${ey})`; } return `${program} Report (Last ${rangeType} weeks)`; }