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)`;
}