first commit

This commit is contained in:
2026-06-16 10:45:41 +07:00
commit ccecc0bc6b
144 changed files with 124547 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

View File

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,20 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());

View File

@@ -0,0 +1,348 @@
Chart.register({
id: 'noDataText',
afterDraw(chart) {
const datasets = chart.data.datasets || [];
const hasData = datasets.some(ds =>
(ds.data || []).some(v => Number(v) > 0)
);
chart.$noData = !hasData;
if (hasData) return;
const { ctx, width, height } = chart;
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '14px sans-serif';
ctx.fillStyle = '#9ca3af';
ctx.fillText('No data available', width / 2, height / 2);
ctx.restore();
}
});
Chart.register({
id: 'centerText',
afterDraw(chart) {
if (chart.config.type !== 'doughnut') return;
if (chart.$noData) return;
const { ctx, chartArea } = chart;
const data = chart.data.datasets[0].data;
const total =
chart.$totalTested ||
chart.$afiTotalCases ||
data.reduce((a, b) => a + b, 0);
if (!chartArea) return;
const centerX = (chartArea.left + chartArea.right) / 2;
const centerY = (chartArea.top + chartArea.bottom) / 2;
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = 'bold 18px sans-serif';
ctx.fillStyle = '#111827';
ctx.fillText(total, centerX, centerY - 8);
ctx.font = '12px sans-serif';
ctx.fillStyle = '#6b7280';
ctx.fillText('Total cases', centerX, centerY + 12);
ctx.restore();
}
});
Chart.register(ChartDataLabels);
Chart.defaults.devicePixelRatio = 2;
const charts = {};
function buildStackedChart(canvasId, labels, data) {
const ctx = document.getElementById(canvasId);
if (!ctx) return;
if (charts[canvasId]) {
charts[canvasId].destroy();
}
charts[canvasId] = new Chart(ctx, {
type: "bar",
data: {
labels: labels,
datasets: data.map(ds => ({
...ds,
borderWidth: 1,
barPercentage: 0.9,
categoryPercentage: 0.8,
maxBarThickness: 60
}))
},
plugins: [ChartDataLabels],
options: {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 20,
bottom: 30
}
},
plugins: {
legend: {
position: 'bottom',
align: 'center',
labels: {
padding: 20,
boxWidth: 10,
boxHeight: 10,
usePointStyle: true,
pointStyle: 'circle'
}
},
// datalabels: {
// color: "#000",
// anchor: "end",
// align: "top",
// clamp: true,
// clip: false,
// font: {
// weight: "bold",
// size: 10
// },
// formatter: function (value) {
// return value > 0 ? value : null;
// }
// }
datalabels: {
display: false
}
},
scales: {
x: {
stacked: true
},
y: {
stacked: true,
beginAtZero: true
}
}
}
});
}
function buildChart(id, type, labels, data) {
const ctx = document.getElementById(id);
if (!ctx) return;
Chart.getChart(id)?.destroy();
const hasData = data && data.some(v => Number(v) > 0);
if (!hasData) {
labels = [];
data = [];
}
const isHorizontal = id === 'sexChart';
const isAgeChart = id === 'ageChart';
const isSentinelChart = id === 'sentinelChart';
const options = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: 30
},
indexAxis: isHorizontal ? 'y' : 'x',
plugins: {
legend: {
position: isAgeChart || isSentinelChart ? 'left' : 'bottom',
align: 'center',
display: (ctx) => {
const chart = ctx.chart;
if (!(chart.config.type === 'pie' || chart.config.type === 'doughnut')) {
return false;
}
return !chart.$noData;
},
labels: {
padding: 14,
boxWidth: 10,
boxHeight: 10,
usePointStyle: true,
pointStyle: 'circle',
font: {
size: 11
}
}
},
datalabels: {
color: "#282626",
offset: 6,
clip: false,
display: (ctx) => {
const chart = ctx.chart;
if (chart.$noData) return false;
if (chart.config.type === 'bar') return true;
return !chart.$noData;
},
anchor: (ctx) => {
const type = ctx.chart.config.type;
if (type === 'doughnut' || type === 'pie') {
return 'center';
}
return 'end';
},
align: (ctx) => {
const type = ctx.chart.config.type;
if (type === 'doughnut' || type === 'pie') {
return 'center';
}
if (type === 'bar') {
return ctx.chart.options.indexAxis === 'y' ? 'right' : 'end';
}
return 'center';
},
font: {
size: 10,
weight: '600'
},
formatter: (value, ctx) => {
if (ctx.chart.$noData) return '';
const data = ctx.chart.data.datasets[0].data;
const total =
ctx.chart.$totalTested ||
data.reduce((a, b) => a + b, 0);
if (!total) return '';
return ((value / total) * 100).toFixed(1) + '%';
}
}
}
};
if (type === 'bar') {
options.scales = {
x: {
beginAtZero: true,
grid: {
display: false
}
},
y: {
grid: {
color: '#f3f4f6'
}
}
};
}
if (type === 'doughnut') {
options.cutout = '70%';
options.maintainAspectRatio = false;
options.elements = {
arc: {
borderWidth: (ctx) => ctx.chart.$noData ? 0 : 1
}
};
}
charts[id] = new Chart(ctx, {
type: type,
data: {
labels: labels,
datasets: [{
data: data,
borderWidth: 2,
tension: 0.3,
barPercentage: 0.8,
categoryPercentage: 0.6,
maxBarThickness: 50
}]
},
options: options
});
charts[id].$totalTested = 0;
}
function buildMixedTrendChart(canvasId, labels, samples, lines) {
const ctx = document.getElementById(canvasId);
if (!ctx) return;
if (charts[canvasId]) charts[canvasId].destroy();
const datasets = [];
lines.forEach(line => {
datasets.push({
type: 'line',
label: line.label,
data: line.data,
borderColor: line.color,
backgroundColor: line.color,
tension: 0.4,
yAxisID: 'y1',
pointStyle: 'line'
});
});
datasets.push({
type: 'bar',
label: 'Total Cases',
data: samples,
backgroundColor: '#007ce8',
maxBarThickness: 60,
yAxisID: 'y',
});
charts[canvasId] = new Chart(ctx, {
data: { labels, datasets },
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom' },
datalabels: {
display: false
}
},
scales: {
y: { title: { display: true, text: 'Cases' } },
y1: {
position: 'right',
grid: { drawOnChartArea: false },
title: { display: true, text: '% Positivity' }
}
}
}
});
}

View File

@@ -0,0 +1,555 @@
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",
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 = `<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: 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)`;
}

View File

@@ -0,0 +1,131 @@
class DashboardFilter {
constructor(onChange) {
this.onChange = onChange;
this.rangeSelect = document.getElementById("trend_range");
this.startYear = document.getElementById("start_year");
this.startWeek = document.getElementById("start_week");
this.endYear = document.getElementById("end_year");
this.endWeek = document.getElementById("end_week");
this.customContainer = document.getElementById("custom_range_container");
if (!this.rangeSelect) return;
this.init();
}
init() {
this.populateFilters();
this.rangeSelect.addEventListener("change", () => {
const val = this.rangeSelect.value;
if (val === "custom") {
this.customContainer.style.display = "flex";
return;
}
this.customContainer.style.display = "none";
const range = this.lastWeeks(parseInt(val));
this.apply(range);
this.trigger();
});
["start_year","start_week","end_year","end_week"].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener("change", ()=>this.trigger());
});
const defaultRange = this.lastWeeks(8);
this.apply(defaultRange);
this.trigger();
}
trigger() {
this.onChange(
this.startYear.value,
this.startWeek.value,
this.endYear.value,
this.endWeek.value
);
}
apply(range) {
this.startYear.value = range.startYear;
this.startWeek.value = range.startWeek;
this.endYear.value = range.endYear;
this.endWeek.value = range.endWeek;
}
populateFilters() {
const year = new Date().getFullYear();
for (let y = year; y >= year-20; y--) {
this.startYear.innerHTML += `<option value="${y}">${y}</option>`;
this.endYear.innerHTML += `<option value="${y}">${y}</option>`;
}
for (let w = 1; w <= 53; w++) {
this.startWeek.innerHTML += `<option value="${w}">W${w}</option>`;
this.endWeek.innerHTML += `<option value="${w}">W${w}</option>`;
}
}
getISOWeek(date) {
const d = new Date(Date.UTC(date.getFullYear(),date.getMonth(),date.getDate()));
d.setUTCDate(d.getUTCDate()+4-(d.getUTCDay()||7));
const yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
const week = Math.ceil((((d-yearStart)/86400000)+1)/7);
return {year:d.getUTCFullYear(),week};
}
lastWeeks(n) {
const end = this.getISOWeek(new Date());
let startWeek = end.week-n+1;
let startYear = end.year;
while(startWeek<=0){
startWeek += 52;
startYear--;
}
return {
startYear,
startWeek,
endYear:end.year,
endWeek:end.week
};
}
}

View File

@@ -0,0 +1,61 @@
export const COLORS = [
'#ef4444',
'#10b981', // emerald
'#f59e0b', // amber
'#ef4444', // red
'#8b5cf6', // violet
'#14b8a6', // teal
'#f97316', // orange
'#84cc16', // lime
'#e11dba', // fuchsia
'#f6f63b', // yellow
'#0ea5e9', // sky
'#22c55e', // green
'#a855f7', // purple
'#ec4899', // pink
'#06b6d4', // cyan
'#65a30d', // olive
'#dc2626', // dark red
'#1d4ed8', // strong blue
'#7c3aed', // deep violet
'#059669', // dark emerald
'#c2410c', // burnt orange
'#be123c', // rose
'#4338ca', // indigo
'#0f766e', // dark teal
'#9333ea', // bright purple
'#15803d', // forest green
'#ea580c', // deep orange
'#0284c7', // ocean blue
'#ca8a04', // mustard
'#db2777' // magenta
];
export const SUBTYPE_COLORS = {
'A/H1N1pdm': '#f0d401',
'A/H3N2': '#00ffff',
'A/H9N2': '#2563eb',
'A/H5N1': '#dc2626',
'A/Unsubtypable': '#f455d7',
'B/Yam': '#9333ea',
'B/Vic': '#086037',
'B/Unsubtypable': '#66ff00',
'B/Victoria': '#9333ea',
'H1N1pdm': '#f0d401',
'H3N2': '#00ffff',
'H9N2': '#2563eb',
'J.2.4': '#8c6060',
'K': '#55f49a',
'Influenza': '#b90c00'
};
export const SURVEILLANCE_COLORS = {
'LBM': '#f0d401',
'ILI': '#2563eb',
'SARI': '#dc2626',
'NDS': '#9333ea',
'AFI': '#086037',
'SEQ': '#66ff00'
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,778 @@
import { COLORS, SUBTYPE_COLORS } from "./globals.js";
let sequencingTotalChart;
let covidLineageFrequencyChart;
let influenzaSubtypeFrequencyChart;
document.addEventListener("DOMContentLoaded", () => {
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
fetch(`/api/dashboard/sequencing?surveillance_id=${window.SURVEILLANCE_ID}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
.then(res => res.json())
.then(data => {
renderSequencingTotalChart(data.trend || []);
});
loadCovidLineageFrequency('week', startYear, startWeek, endYear, endWeek)
loadInfluenzaSubtypeFrequency('week', startYear, startWeek, endYear, endWeek)
const elements = document.querySelectorAll(".report-period");
elements.forEach(el => {
el.textContent = 'Week ' + startWeek + ' of ' + startYear + ' to ' + 'Week ' + endWeek + ' of ' + endYear
});
});
});
function renderSequencingTotalChart(rows) {
const ctx = document.getElementById('sequencingTotalChart')?.getContext('2d');
if (!ctx) return;
if (sequencingTotalChart) sequencingTotalChart.destroy();
const totals = {};
rows.forEach(r => {
totals[r.period] = (totals[r.period] || 0) + Number(r.total);
});
const weeks = Object.keys(totals);
const values = Object.values(totals);
sequencingTotalChart = new Chart(ctx, {
type: 'bar',
data: {
labels: weeks.map(w => `${w}`),
datasets: [{
label: 'Total Samples',
data: values,
backgroundColor: '#0B8F3C'
}]
},
options: {
maintainAspectRatio: false,
plugins: {
legend: { display: false },
datalabels: {
display: false
}
},
}
});
charts['sequencingTotalChart'] = sequencingTotalChart;
}
function hexToRGBA(hex, alpha) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, endWeek) {
fetch(`/api/dashboard/covid-lineage-frequency?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
.then(res => res.json())
.then(data => {
// const weeks = [...new Set(data.map(item => item.week))].sort();
const totalYears = endYear - startYear;
const useYearlyView = totalYears >= 5;
let periods;
if (useYearlyView) {
periods = [...new Set(
data.map(item => item.week.split('-')[0])
)].sort((a, b) => a - b);
} else {
periods = [...new Set(
data.map(item => item.week)
)].sort();
}
const lineages = [...new Set(data.map(item => item.lineage))];
const datasets = lineages.map((lineage, index) => {
// const lineageData = weeks.map(week => {
// const found = data.find(
// item => item.week === week && item.lineage === lineage
// );
// return found ? found.total : 0;
// });
const lineageData = periods.map(period => {
if (useYearlyView) {
return data
.filter(item =>
item.week.split('-')[0] == period &&
item.lineage === lineage
)
.reduce((sum, item) => sum + item.total, 0);
}
const found = data.find(
item =>
item.week === period &&
item.lineage === lineage
);
return found ? found.total : 0;
});
return {
label: lineage,
data: lineageData,
fill: true,
tension: 0.4,
// borderColor: 'transparent',
borderWidth: 0,
pointRadius: 0,
backgroundColor: hexToRGBA(COLORS[index % COLORS.length], 0.3),
stack: 'total'
};
});
if (covidLineageFrequencyChart) covidLineageFrequencyChart.destroy();
const ctx = document.getElementById('covidLineageFrequency').getContext('2d');
covidLineageFrequencyChart = new Chart(ctx, {
type: 'line',
data: {
labels: periods,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false
},
datalabels: {
display: false
}
},
scales: {
x: {
stacked: true,
grid: {
display: false
}
},
y: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: 'Relative Frequency'
},
}
}
}
});
charts['covidLineageFrequency'] = covidLineageFrequencyChart;
// -------------------------
// Custom right-side scrollable legend
// -------------------------
const legendContainer = document.getElementById('legendContainer');
legendContainer.innerHTML = '';
datasets.forEach((dataset, index) => {
const item = document.createElement('div');
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.marginBottom = '4px';
item.style.fontSize = '11px';
item.style.cursor = 'pointer';
item.innerHTML = `
<span style="width:15px;height:15px;background:${dataset.backgroundColor};display:inline-block;margin-right:8px;"></span>
${dataset.label}
`;
item.addEventListener('click', () => {
const meta = covidLineageFrequencyChart.getDatasetMeta(index);
const allHidden = datasets.every((d, i) => covidLineageFrequencyChart.getDatasetMeta(i).hidden || i === index);
if (!allHidden) {
datasets.forEach((d, i) => {
covidLineageFrequencyChart.getDatasetMeta(i).hidden = true;
});
meta.hidden = false;
} else {
datasets.forEach((d, i) => {
covidLineageFrequencyChart.getDatasetMeta(i).hidden = false;
});
}
covidLineageFrequencyChart.update();
Array.from(legendContainer.children).forEach((child, i) => {
const metaItem = covidLineageFrequencyChart.getDatasetMeta(i);
child.style.opacity = metaItem.hidden ? 0.5 : 1;
});
});
legendContainer.appendChild(item);
});
legendContainer.style.maxHeight = '375px';
legendContainer.style.overflowY = 'auto';
legendContainer.style.padding = '8px';
legendContainer.style.borderRadius = '0px';
});
}
function loadInfluenzaSubtypeFrequency(periodType, startYear, startWeek, endYear, endWeek) {
fetch(`/api/dashboard/influenza-relative-frequency-sequencing?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
.then(res => res.json())
.then(data => {
const totalYears = endYear - startYear;
const useYearlyView = totalYears >= 5;
let periods;
if (useYearlyView) {
periods = [...new Set(
data.map(item => item.week.split('-')[0])
)].sort((a, b) => a - b);
} else {
periods = [...new Set(
data.map(item => item.week)
)].sort();
}
const lineages = [...new Set(
data.map(item => item.lineage)
)];
const lineageColors = lineages.map(
label => SUBTYPE_COLORS[label] || '#9ca3af'
);
const datasets = lineages.map((lineage, index) => {
const lineageData = periods.map(period => {
if (useYearlyView) {
return data
.filter(item =>
item.week.split('-')[0] == period &&
item.lineage === lineage
)
.reduce((sum, item) => sum + item.total, 0);
}
const found = data.find(
item =>
item.week === period &&
item.lineage === lineage
);
return found ? found.total : 0;
});
return {
label: lineage,
data: lineageData,
fill: true,
tension: 0.4,
borderWidth: 0,
pointRadius: 0,
backgroundColor: hexToRGBA(
lineageColors[index % lineageColors.length],
0.6
),
stack: 'total'
};
});
if (influenzaSubtypeFrequencyChart) {
influenzaSubtypeFrequencyChart.destroy();
}
const ctx = document
.getElementById('influenzaSubtypeFrequency')
.getContext('2d');
influenzaSubtypeFrequencyChart = new Chart(ctx, {
type: 'line',
data: {
labels: periods,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false
},
datalabels: {
display: false
}
},
scales: {
x: {
stacked: true,
grid: {
display: false
}
},
y: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: 'Relative Frequency'
}
}
}
}
});
charts['influenzaSubtypeFrequency'] =
influenzaSubtypeFrequencyChart;
/*
|--------------------------------------------------------------------------
| CUSTOM LEGEND
|--------------------------------------------------------------------------
*/
const legendContainer =
document.getElementById(
'legendContainerInfluenzaSubtypeFrequency'
);
legendContainer.innerHTML = '';
datasets.forEach((dataset, index) => {
const item = document.createElement('div');
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.marginBottom = '4px';
item.style.fontSize = '11px';
item.style.cursor = 'pointer';
item.innerHTML = `
<span style="
width:15px;
height:15px;
background:${dataset.backgroundColor};
display:inline-block;
margin-right:8px;
"></span>
${dataset.label}
`;
item.addEventListener('click', () => {
const meta =
influenzaSubtypeFrequencyChart
.getDatasetMeta(index);
const allHidden = datasets.every(
(d, i) =>
influenzaSubtypeFrequencyChart
.getDatasetMeta(i).hidden ||
i === index
);
if (!allHidden) {
datasets.forEach((d, i) => {
influenzaSubtypeFrequencyChart
.getDatasetMeta(i)
.hidden = true;
});
meta.hidden = false;
} else {
datasets.forEach((d, i) => {
influenzaSubtypeFrequencyChart
.getDatasetMeta(i)
.hidden = false;
});
}
influenzaSubtypeFrequencyChart.update();
Array.from(legendContainer.children)
.forEach((child, i) => {
const metaItem =
influenzaSubtypeFrequencyChart
.getDatasetMeta(i);
child.style.opacity =
metaItem.hidden ? 0.5 : 1;
});
});
legendContainer.appendChild(item);
});
legendContainer.style.maxHeight = '375px';
legendContainer.style.overflowY = 'auto';
legendContainer.style.padding = '8px';
legendContainer.style.borderRadius = '0px';
});
}
// function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, endWeek) {
// fetch(`/api/dashboard/covid-lineage-frequency?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
// .then(res => res.json())
// .then(data => {
// const weeks = [...new Set(data.map(item => item.week))].sort();
// const lineages = [...new Set(data.map(item => item.lineage))];
// const datasets = lineages.map((lineage, index) => {
// const lineageData = weeks.map(week => {
// const found = data.find(
// item => item.week === week && item.lineage === lineage
// );
// return found ? found.total : 0;
// });
// return {
// label: lineage,
// data: lineageData,
// fill: true,
// tension: 0.4,
// borderColor: 'transparent',
// borderWidth: 0,
// pointRadius: 0,
// backgroundColor: hexToRGBA(colors[index % colors.length], 0.3),
// stack: 'total'
// };
// });
// if (covidLineageFrequencyChart) covidLineageFrequencyChart.destroy();
// const ctx = document.getElementById('covidLineageFrequency').getContext('2d');
// if (!ctx) return;
// covidLineageFrequencyChart = new Chart(ctx, {
// type: 'line',
// data: {
// labels: weeks,
// datasets: datasets
// },
// options: {
// responsive: true,
// maintainAspectRatio: false,
// interaction: {
// mode: 'index',
// intersect: false
// },
// plugins: {
// legend: {
// display: false // hide default legend
// },
// tooltip: {
// mode: 'index',
// intersect: false
// },
// datalabels: {
// display: false // hide labels
// }
// },
// scales: {
// x: {
// stacked: true,
// title: {
// display: true,
// text: 'Week'
// },
// grid: {
// display: false
// }
// },
// y: {
// stacked: true,
// beginAtZero: true,
// title: {
// display: true,
// text: 'Relative Frequency'
// },
// }
// }
// }
// });
// charts['covidLineageFrequencyChart'] = covidLineageFrequencyChart;
// // -------------------------
// // Custom right-side scrollable legend
// // -------------------------
// const legendContainer = document.getElementById('legendContainer');
// legendContainer.innerHTML = '';
// datasets.forEach((dataset, index) => {
// const item = document.createElement('div');
// item.style.display = 'flex';
// item.style.alignItems = 'center';
// item.style.marginBottom = '4px';
// item.style.fontSize = '11px';
// item.style.cursor = 'pointer';
// item.innerHTML = `
// <span style="width:15px;height:15px;background:${dataset.backgroundColor};display:inline-block;margin-right:8px;"></span>
// ${dataset.label}
// `;
// item.addEventListener('click', () => {
// const meta = covidLineageFrequencyChart.getDatasetMeta(index);
// const allHidden = datasets.every((d, i) => covidLineageFrequencyChart.getDatasetMeta(i).hidden || i === index);
// if (!allHidden) {
// datasets.forEach((d, i) => {
// covidLineageFrequencyChart.getDatasetMeta(i).hidden = true;
// });
// meta.hidden = false;
// } else {
// datasets.forEach((d, i) => {
// covidLineageFrequencyChart.getDatasetMeta(i).hidden = false;
// });
// }
// covidLineageFrequencyChart.update();
// Array.from(legendContainer.children).forEach((child, i) => {
// const metaItem = covidLineageFrequencyChart.getDatasetMeta(i);
// child.style.opacity = metaItem.hidden ? 0.5 : 1;
// });
// });
// legendContainer.appendChild(item);
// });
// legendContainer.style.maxHeight = '375px';
// legendContainer.style.overflowY = 'auto';
// legendContainer.style.padding = '8px';
// legendContainer.style.borderRadius = '0px';
// });
// }
// function loadInfluenzaSubtypeFrequency(periodType, startYear, startWeek, endYear, endWeek) {
// fetch(`/api/dashboard/influenza-relative-frequency-sequencing?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
// .then(res => res.json())
// .then(data => {
// const weeks = [...new Set(data.map(item => item.week))].sort();
// const lineages = [...new Set(data.map(item => item.lineage))];
// const datasets = lineages.map((lineage, index) => {
// const lineageData = weeks.map(week => {
// const found = data.find(
// item => item.week === week && item.lineage === lineage
// );
// return found ? found.total : 0;
// });
// return {
// label: lineage,
// data: lineageData,
// fill: true,
// tension: 0.4,
// borderColor: 'transparent',
// borderWidth: 0,
// pointRadius: 0,
// backgroundColor: hexToRGBA(colors[(index * 2) % colors.length], 0.8),
// stack: 'total'
// };
// });
// if (influenzaSubtypeFrequencyChart) influenzaSubtypeFrequencyChart.destroy();
// const ctx = document.getElementById('influenzaSubtypeFrequency').getContext('2d');
// if (!ctx) return;
// influenzaSubtypeFrequencyChart = new Chart(ctx, {
// type: 'line',
// data: {
// labels: weeks,
// datasets: datasets
// },
// options: {
// responsive: true,
// maintainAspectRatio: false,
// interaction: {
// mode: 'index',
// intersect: false
// },
// plugins: {
// legend: {
// display: false
// },
// tooltip: {
// mode: 'index',
// intersect: false
// },
// datalabels: {
// display: false
// }
// },
// scales: {
// x: {
// stacked: true,
// title: {
// display: true,
// text: 'Week'
// },
// grid: {
// display: false
// }
// },
// y: {
// stacked: true,
// beginAtZero: true,
// title: {
// display: true,
// text: 'Relative Frequency'
// },
// }
// }
// }
// });
// charts['influenzaSubtypeFrequencyChart'] = influenzaSubtypeFrequencyChart;
// const legendContainer = document.getElementById('legendContainerInfluenzaSubtypeFrequency');
// legendContainer.innerHTML = '';
// datasets.forEach((dataset, index) => {
// const item = document.createElement('div');
// item.style.display = 'flex';
// item.style.alignItems = 'center';
// item.style.marginBottom = '4px';
// item.style.fontSize = '11px';
// item.style.cursor = 'pointer';
// item.innerHTML = `
// <span style="width:15px;height:15px;background:${dataset.backgroundColor};display:inline-block;margin-right:8px;"></span>
// ${dataset.label}
// `;
// item.addEventListener('click', () => {
// const meta = influenzaSubtypeFrequencyChart.getDatasetMeta(index);
// const allHidden = datasets.every((d, i) => influenzaSubtypeFrequencyChart.getDatasetMeta(i).hidden || i === index);
// if (!allHidden) {
// datasets.forEach((d, i) => {
// influenzaSubtypeFrequencyChart.getDatasetMeta(i).hidden = true;
// });
// meta.hidden = false;
// } else {
// datasets.forEach((d, i) => {
// influenzaSubtypeFrequencyChart.getDatasetMeta(i).hidden = false;
// });
// }
// influenzaSubtypeFrequencyChart.update();
// Array.from(legendContainer.children).forEach((child, i) => {
// const metaItem = influenzaSubtypeFrequencyChart.getDatasetMeta(i);
// child.style.opacity = metaItem.hidden ? 0.5 : 1;
// });
// });
// legendContainer.appendChild(item);
// });
// legendContainer.style.maxHeight = '375px';
// legendContainer.style.overflowY = 'auto';
// legendContainer.style.padding = '8px';
// legendContainer.style.borderRadius = '0px';
// });
// }

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: