458 lines
12 KiB
JavaScript
458 lines
12 KiB
JavaScript
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 += `
|
||
<label style="display:block;margin-bottom:6px;">
|
||
<input type="checkbox" value="${id}" checked>
|
||
${formatChartName(id)}
|
||
</label>
|
||
`;
|
||
});
|
||
list.innerHTML += `
|
||
<label>
|
||
<input type="checkbox" value="provinceMap" checked>
|
||
Geographic Distribution
|
||
</label>
|
||
`;
|
||
|
||
modal.style.display = "block";
|
||
}
|
||
|
||
function closeChartSelector() {
|
||
document.getElementById("chartModal").style.display = "none";
|
||
}
|
||
|
||
function formatChartName(id) {
|
||
const map = {
|
||
trendChart: "Case Trend & Positivity",
|
||
pathogenChart: "Pathogen Distribution",
|
||
provinceMap: "Geographic Distribution",
|
||
ageChart: "Age Distribution",
|
||
sexChart: "Sex Distribution",
|
||
subtypeChart: "Influenza Subtypes",
|
||
sentinelChart: "Sentinel Site Distribution"
|
||
};
|
||
|
||
return map[id] || id;
|
||
}
|
||
|
||
async function exportSelectedCharts() {
|
||
console.log("Exporting selected charts...");
|
||
|
||
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 tempCanvas = document.createElement("canvas");
|
||
tempCanvas.width = canvas.width;
|
||
tempCanvas.height = canvas.height;
|
||
|
||
const ctx = tempCanvas.getContext("2d", { willReadFrequently: true });
|
||
ctx.fillStyle = "#ffffff";
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
ctx.drawImage(canvas, 0, 0);
|
||
|
||
img = tempCanvas.toDataURL("image/png");
|
||
|
||
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]
|
||
]);
|
||
|
||
map.fitBounds(bounds);
|
||
|
||
}
|
||
async function exportFullDashboard() {
|
||
console.log("Exporting full charts...");
|
||
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 = `<img src="${mapImg}" style="width:100%;height:100%;object-fit:cover;" />`;
|
||
}
|
||
}
|
||
prepareMapForExport();
|
||
|
||
await new Promise(resolve => setTimeout(resolve, 800));
|
||
|
||
const canvas = await html2canvas(el, {
|
||
scale: 3,
|
||
useCORS: true,
|
||
scrollY: -window.scrollY
|
||
});
|
||
|
||
if (mapEl && originalMapHTML !== null) {
|
||
mapEl.innerHTML = originalMapHTML;
|
||
map.invalidateSize();
|
||
}
|
||
|
||
const img = canvas.toDataURL("image/png");
|
||
|
||
const { jsPDF } = window.jspdf;
|
||
const pdf = new jsPDF("l", "mm", "a4");
|
||
|
||
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, "PNG", 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 = map.getZoom();
|
||
const projection = 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 = normalizeProvince(r.patient_province, window.validProvinces);
|
||
console.log(province, totals[province]);
|
||
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";
|
||
}
|
||
|
||
map.eachLayer(layer => {
|
||
if (!layer.toGeoJSON) return;
|
||
|
||
const geo = layer.toGeoJSON();
|
||
|
||
const features = geo.type === "FeatureCollection"
|
||
? geo.features
|
||
: [geo];
|
||
|
||
features.forEach(f => {
|
||
if (!f.geometry) return;
|
||
|
||
const coords = f.geometry.coordinates;
|
||
|
||
const polygons = f.geometry.type === "MultiPolygon"
|
||
? coords
|
||
: [coords];
|
||
|
||
polygons.forEach(poly => {
|
||
poly.forEach(ring => {
|
||
|
||
const projected = ring.map(([lng, lat]) => {
|
||
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];
|
||
});
|
||
|
||
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(({ points, properties }) => {
|
||
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)`;
|
||
} |