1025 lines
24 KiB
JavaScript
1025 lines
24 KiB
JavaScript
import { COLORS, SUBTYPE_COLORS } from "./globals.js";
|
||
const standardPrograms = ['SARI', 'ILI', 'LBM', 'AFI', 'NDS'];
|
||
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
|
||
|
||
let map;
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
|
||
if (!standardPrograms.includes(programCode)) return;
|
||
|
||
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
|
||
|
||
document.querySelectorAll(".report-period")
|
||
.forEach(el => {
|
||
el.textContent =
|
||
`Week ${startWeek} of ${startYear} to Week ${endWeek} of ${endYear}`;
|
||
});
|
||
|
||
fetch(
|
||
`/api/dashboard/program?surveillance_id=${window.SURVEILLANCE_ID}` +
|
||
`&start_year=${startYear}&start_week=${startWeek}` +
|
||
`&end_year=${endYear}&end_week=${endWeek}`
|
||
)
|
||
.then(res => res.json())
|
||
.then(programCode === 'AFI'
|
||
? renderAFIDashboard
|
||
: renderDashboard
|
||
)
|
||
.catch(err => console.error("Dashboard API error:", err));
|
||
|
||
});
|
||
|
||
});
|
||
function buildDistributionChart(
|
||
id,
|
||
type,
|
||
rows,
|
||
labelKey,
|
||
valueKey = 'total',
|
||
colorResolver = null
|
||
) {
|
||
|
||
const labels = rows.map(r => r[labelKey]);
|
||
buildChart(
|
||
id,
|
||
type,
|
||
labels,
|
||
rows.map(r => r[valueKey])
|
||
);
|
||
|
||
if (!charts[id]) return;
|
||
|
||
charts[id].data.datasets[0].backgroundColor = labels.map(
|
||
(label, index) =>
|
||
colorResolver
|
||
? colorResolver(label, index)
|
||
: COLORS[index % COLORS.length]
|
||
);
|
||
|
||
charts[id].update();
|
||
}
|
||
function renderTrend(valueId, changeId, current, previous, suffix = '') {
|
||
|
||
const valueEl = document.getElementById(valueId);
|
||
const changeEl = document.getElementById(changeId);
|
||
|
||
if (!valueEl || !changeEl) return;
|
||
|
||
valueEl.textContent = current + suffix;
|
||
|
||
if (!previous) {
|
||
changeEl.innerHTML = "— No previous data";
|
||
changeEl.className = "text-muted";
|
||
return;
|
||
}
|
||
|
||
const diff = current - previous;
|
||
const percent = ((diff / previous) * 100).toFixed(1);
|
||
|
||
if (diff > 0) {
|
||
changeEl.innerHTML = `↑ +${percent}% from previous week`;
|
||
changeEl.className = "text-success";
|
||
}
|
||
else if (diff < 0) {
|
||
changeEl.innerHTML = `↓ ${percent}% from previous week`;
|
||
changeEl.className = "text-danger";
|
||
}
|
||
else {
|
||
changeEl.innerHTML = "— No significant change";
|
||
changeEl.className = "text-muted";
|
||
}
|
||
}
|
||
function getSubtypeColor(label, index = 0) {
|
||
|
||
const specialColors = {
|
||
'A/H5N1': '#dc2626'
|
||
};
|
||
|
||
return specialColors[label]
|
||
|| COLORS[index % COLORS.length];
|
||
}
|
||
function renderSummary(summary = {}) {
|
||
|
||
const mappings = [
|
||
['totalCases', 'casesChange', summary.cases],
|
||
['influenzaRate', 'influenzaChange', summary.influenza_rate, '%'],
|
||
['covidRate', 'covidChange', summary.covid_rate, '%'],
|
||
['hospitalRate', 'hospitalChange', summary.hospital_rate, '%'],
|
||
['icuRate', 'icuChange', summary.icu_rate, '%'],
|
||
['positivityRate', 'positivityChange', summary.positivity_rate, '%']
|
||
];
|
||
|
||
mappings.forEach(([valueId, changeId, data = {}, suffix = '']) => {
|
||
renderTrend(
|
||
valueId,
|
||
changeId,
|
||
data.current || 0,
|
||
data.previous || 0,
|
||
suffix
|
||
);
|
||
});
|
||
}
|
||
function renderDashboard(data = {}) {
|
||
|
||
renderProgramTrend(data.trend || []);
|
||
renderSummary(data.summary);
|
||
renderProvinceHeatmap(data.province_distribution || []);
|
||
|
||
buildDistributionChart(
|
||
'pathogenChart',
|
||
'doughnut',
|
||
(data.pathogen_distribution || [])
|
||
.sort((a, b) => b.total - a.total),
|
||
'pathogen'
|
||
);
|
||
|
||
buildDistributionChart(
|
||
'ageChart',
|
||
'doughnut',
|
||
data.age_distribution || [],
|
||
'age_group'
|
||
);
|
||
|
||
buildDistributionChart(
|
||
'sexChart',
|
||
'bar',
|
||
data.sex_distribution || [],
|
||
'patient_sex'
|
||
);
|
||
|
||
buildDistributionChart(
|
||
'subtypeChart',
|
||
'bar',
|
||
data.subtype_distribution || [],
|
||
'subtype',
|
||
'total',
|
||
getSubtypeColor
|
||
);
|
||
|
||
buildDistributionChart(
|
||
'sentinelChart',
|
||
'pie',
|
||
data.sentinel_sites || [],
|
||
'name'
|
||
);
|
||
|
||
|
||
}
|
||
function renderAFIDashboard(data = {}) {
|
||
|
||
const trend = data.afi_trend || {};
|
||
const positivity = data.afi_case_trend || {};
|
||
|
||
renderAFITrend(
|
||
data.afi_case_trend.section_1,
|
||
'afiSection1Trend',
|
||
COLORS,
|
||
'trend'
|
||
);
|
||
|
||
renderAFITrend(
|
||
data.afi_case_trend.section_2,
|
||
'afiSection2Trend',
|
||
COLORS,
|
||
'trend'
|
||
);
|
||
|
||
renderAFITrend(
|
||
data.afi_case_trend.section_3,
|
||
'afiSection3Trend',
|
||
COLORS,
|
||
'trend'
|
||
);
|
||
|
||
// DOUGHNUTS
|
||
renderAFITrend(
|
||
data.afi_case_trend.section_1,
|
||
'afiPcrChart',
|
||
COLORS,
|
||
'donut'
|
||
);
|
||
|
||
renderAFITrend(
|
||
data.afi_case_trend.section_2,
|
||
'afiMultiplexChart',
|
||
COLORS,
|
||
'donut'
|
||
);
|
||
|
||
renderAFITrend(
|
||
data.afi_case_trend.section_3,
|
||
'afiElisaChart',
|
||
COLORS,
|
||
'donut'
|
||
);
|
||
|
||
|
||
renderSummary(data.summary);
|
||
renderProvinceHeatmap(data.province_distribution);
|
||
renderDemographics(data);
|
||
renderPathogenChart(data.pathogen_distribution || []);
|
||
renderSentinel(data.sentinel_sites || []);
|
||
renderSubtypeChart(data.subtype_distribution || []);
|
||
}
|
||
function normalizeProvince(name, validSet) {
|
||
|
||
if (!name || !validSet) return null;
|
||
|
||
const clean = str =>
|
||
str.toLowerCase().replace(/\s+/g, '');
|
||
|
||
const raw = name.trim();
|
||
|
||
const provinceMap = {
|
||
"kepville": "Kep",
|
||
"sihanoukville": "Preah Sihanouk",
|
||
"sihanoukvillecity": "Preah Sihanouk",
|
||
"krongpailin": "Pailin",
|
||
"mondulkiri": "Mondulkiri",
|
||
"odormeanchey": "Oddar Meanchey",
|
||
"tbongkhmom": "Tboung Khmum",
|
||
"tboungkhmum": "Tboung Khmum",
|
||
"rattanakiri": "Ratanak Kiri"
|
||
};
|
||
|
||
const key = clean(raw);
|
||
|
||
if (provinceMap[key] && validSet.has(provinceMap[key])) {
|
||
return provinceMap[key];
|
||
}
|
||
|
||
const match = [...validSet]
|
||
.find(p => clean(p) === key);
|
||
|
||
return match || null;
|
||
}
|
||
function renderProvinceHeatmap(rows = []) {
|
||
|
||
window.latestProvinceData = rows;
|
||
|
||
if (map) {
|
||
map.remove();
|
||
}
|
||
|
||
map = L.map('provinceMap')
|
||
.setView([12.7, 104.9], 7);
|
||
|
||
L.tileLayer(
|
||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||
{
|
||
attribution: '© OpenStreetMap contributors'
|
||
}
|
||
).addTo(map);
|
||
|
||
addProvinceLegend();
|
||
|
||
fetch('/geo/cambodia_provinces.geojson')
|
||
.then(r => r.json())
|
||
.then(geo => {
|
||
|
||
const validProvinces = new Set(
|
||
geo.features.map(f => f.properties.ADM1_EN)
|
||
);
|
||
|
||
window.validProvinces = validProvinces;
|
||
|
||
const totals = {};
|
||
|
||
rows.forEach(r => {
|
||
|
||
const province = normalizeProvince(
|
||
r.patient_province,
|
||
validProvinces
|
||
);
|
||
|
||
if (!province) return;
|
||
|
||
totals[province] ??= {
|
||
total: 0,
|
||
positive: 0
|
||
};
|
||
|
||
totals[province].total += Number(r.total || 0);
|
||
totals[province].positive += Number(r.positive || 0);
|
||
|
||
});
|
||
|
||
const getColor = value => {
|
||
|
||
if (value > 50) return "#b91c1c";
|
||
if (value >= 10) return "#ef4444";
|
||
if (value > 0) return "#fecaca";
|
||
|
||
return "#f3f4f600";
|
||
};
|
||
|
||
window.provinceLayer = L.geoJSON(geo, {
|
||
|
||
style: feature => {
|
||
|
||
const province =
|
||
feature.properties.ADM1_EN;
|
||
|
||
const value =
|
||
totals[province]?.total || 0;
|
||
|
||
return {
|
||
color: "#444",
|
||
weight: 1.5,
|
||
fillColor: getColor(value),
|
||
fillOpacity: 0.8
|
||
};
|
||
},
|
||
|
||
onEachFeature: (feature, layer) => {
|
||
|
||
const province =
|
||
feature.properties.ADM1_EN;
|
||
|
||
const total =
|
||
totals[province]?.total || 0;
|
||
|
||
const positive =
|
||
totals[province]?.positive || 0;
|
||
|
||
const percent = total
|
||
? ((positive / total) * 100).toFixed(1)
|
||
: 0;
|
||
|
||
layer.bindTooltip(`
|
||
${province}<br>
|
||
Total: ${total}<br>
|
||
Positivity: ${percent}%
|
||
`);
|
||
|
||
}
|
||
|
||
}).addTo(map);
|
||
|
||
});
|
||
|
||
}
|
||
function addProvinceLegend() {
|
||
|
||
const legend = L.control({
|
||
position: "bottomright"
|
||
});
|
||
|
||
legend.onAdd = function () {
|
||
|
||
const div = L.DomUtil.create(
|
||
"div",
|
||
"map-legend"
|
||
);
|
||
|
||
div.innerHTML = `
|
||
<div style="
|
||
background:white;
|
||
padding:10px 12px;
|
||
border-radius:6px;
|
||
box-shadow:0 2px 6px rgba(0,0,0,0.2);
|
||
font-size:12px;
|
||
">
|
||
|
||
<div style="
|
||
font-weight:600;
|
||
margin-bottom:6px;
|
||
">
|
||
Cases
|
||
</div>
|
||
|
||
<div style="
|
||
display:flex;
|
||
align-items:center;
|
||
margin-bottom:4px;
|
||
">
|
||
<span style="
|
||
width:12px;
|
||
height:12px;
|
||
background:#b91c1c;
|
||
display:inline-block;
|
||
margin-right:6px;
|
||
"></span>
|
||
> 50
|
||
</div>
|
||
|
||
<div style="
|
||
display:flex;
|
||
align-items:center;
|
||
margin-bottom:4px;
|
||
">
|
||
<span style="
|
||
width:12px;
|
||
height:12px;
|
||
background:#ef4444;
|
||
display:inline-block;
|
||
margin-right:6px;
|
||
"></span>
|
||
10 – 50
|
||
</div>
|
||
|
||
<div style="
|
||
display:flex;
|
||
align-items:center;
|
||
margin-bottom:4px;
|
||
">
|
||
<span style="
|
||
width:12px;
|
||
height:12px;
|
||
background:#fecaca;
|
||
display:inline-block;
|
||
margin-right:6px;
|
||
"></span>
|
||
1 – 9
|
||
</div>
|
||
|
||
<div style="
|
||
display:flex;
|
||
align-items:center;
|
||
">
|
||
<span style="
|
||
width:12px;
|
||
height:12px;
|
||
background:#f3f4f6;
|
||
display:inline-block;
|
||
margin-right:6px;
|
||
"></span>
|
||
0
|
||
</div>
|
||
|
||
</div>
|
||
`;
|
||
|
||
return div;
|
||
};
|
||
|
||
legend.addTo(map);
|
||
}
|
||
function renderProgramTrend(rows = []) {
|
||
|
||
if (!rows.length) {
|
||
|
||
buildMixedTrendChart(
|
||
'trendChart',
|
||
[],
|
||
[],
|
||
[]
|
||
);
|
||
|
||
return;
|
||
}
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| YEARLY VIEW
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
const years = [...new Set(
|
||
rows.map(r => Number(r.year))
|
||
)];
|
||
|
||
const totalYears =
|
||
Math.max(...years) - Math.min(...years);
|
||
|
||
const useYearlyView =
|
||
totalYears >= 5;
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| LABELS
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
let labels;
|
||
|
||
if (useYearlyView) {
|
||
|
||
labels = [...new Set(
|
||
rows.map(r => String(r.year))
|
||
)].sort();
|
||
|
||
} else {
|
||
|
||
labels = [...new Set(
|
||
rows.map(r =>
|
||
`${r.year}-W${r.period}`
|
||
)
|
||
)].sort((a, b) => {
|
||
|
||
const [yearA, weekA] =
|
||
a.split('-W').map(Number);
|
||
|
||
const [yearB, weekB] =
|
||
b.split('-W').map(Number);
|
||
|
||
if (yearA !== yearB) {
|
||
return yearA - yearB;
|
||
}
|
||
|
||
return weekA - weekB;
|
||
|
||
});
|
||
|
||
}
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| TOTAL SAMPLES
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
const samples = labels.map(label => {
|
||
|
||
if (useYearlyView) {
|
||
|
||
return rows
|
||
.filter(r =>
|
||
String(r.year) === label
|
||
)
|
||
.reduce(
|
||
(sum, r) =>
|
||
sum + Number(r.total_samples || 0),
|
||
0
|
||
);
|
||
|
||
}
|
||
|
||
const row = rows.find(r =>
|
||
`${r.year}-W${r.period}` === label
|
||
);
|
||
|
||
return row?.total_samples || 0;
|
||
|
||
});
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| LINES
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
const lines = [
|
||
|
||
{
|
||
label: 'Influenza %',
|
||
|
||
data: labels.map(label => {
|
||
|
||
if (useYearlyView) {
|
||
|
||
const filtered = rows.filter(r =>
|
||
String(r.year) === label
|
||
);
|
||
|
||
const total =
|
||
filtered.reduce(
|
||
(sum, r) =>
|
||
sum + Number(r.influenza_rate || 0),
|
||
0
|
||
);
|
||
|
||
return filtered.length
|
||
? total / filtered.length
|
||
: 0;
|
||
}
|
||
|
||
const row = rows.find(r =>
|
||
`${r.year}-W${r.period}` === label
|
||
);
|
||
|
||
return row?.influenza_rate || 0;
|
||
|
||
}),
|
||
|
||
color: '#1976D2'
|
||
},
|
||
|
||
{
|
||
label: 'COVID-19 %',
|
||
|
||
data: labels.map(label => {
|
||
|
||
if (useYearlyView) {
|
||
|
||
const filtered = rows.filter(r =>
|
||
String(r.year) === label
|
||
);
|
||
|
||
const total =
|
||
filtered.reduce(
|
||
(sum, r) =>
|
||
sum + Number(r.covid_rate || 0),
|
||
0
|
||
);
|
||
|
||
return filtered.length
|
||
? total / filtered.length
|
||
: 0;
|
||
}
|
||
|
||
const row = rows.find(r =>
|
||
`${r.year}-W${r.period}` === label
|
||
);
|
||
|
||
return row?.covid_rate || 0;
|
||
|
||
}),
|
||
|
||
color: '#10b981'
|
||
}
|
||
|
||
];
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| NDS EXTRA LINES
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
if (programCode === 'NDS') {
|
||
|
||
lines.push(
|
||
|
||
{
|
||
label: 'EV %',
|
||
|
||
data: labels.map(label => {
|
||
|
||
if (useYearlyView) {
|
||
|
||
const filtered = rows.filter(r =>
|
||
String(r.year) === label
|
||
);
|
||
|
||
const total =
|
||
filtered.reduce(
|
||
(sum, r) =>
|
||
sum + Number(r.ev_rate || 0),
|
||
0
|
||
);
|
||
|
||
return filtered.length
|
||
? total / filtered.length
|
||
: 0;
|
||
}
|
||
|
||
const row = rows.find(r =>
|
||
`${r.year}-W${r.period}` === label
|
||
);
|
||
|
||
return row?.ev_rate || 0;
|
||
|
||
}),
|
||
|
||
color: '#f59e0b'
|
||
},
|
||
|
||
{
|
||
label: 'Mpox %',
|
||
|
||
data: labels.map(label => {
|
||
|
||
if (useYearlyView) {
|
||
|
||
const filtered = rows.filter(r =>
|
||
String(r.year) === label
|
||
);
|
||
|
||
const total =
|
||
filtered.reduce(
|
||
(sum, r) =>
|
||
sum + Number(r.mpox_rate || 0),
|
||
0
|
||
);
|
||
|
||
return filtered.length
|
||
? total / filtered.length
|
||
: 0;
|
||
}
|
||
|
||
const row = rows.find(r =>
|
||
`${r.year}-W${r.period}` === label
|
||
);
|
||
|
||
return row?.mpox_rate || 0;
|
||
|
||
}),
|
||
|
||
color: '#8b5cf6'
|
||
}
|
||
|
||
);
|
||
|
||
}
|
||
|
||
buildMixedTrendChart(
|
||
'trendChart',
|
||
labels,
|
||
samples,
|
||
lines
|
||
);
|
||
|
||
}
|
||
function renderAFITrend(
|
||
rows = [],
|
||
canvasId,
|
||
COLORS,
|
||
type = 'trend'
|
||
) {
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| EMPTY
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
if (!rows.length) {
|
||
|
||
if (type === 'donut') {
|
||
|
||
buildDistributionChart(
|
||
canvasId,
|
||
'doughnut',
|
||
[],
|
||
'pathogen',
|
||
'total'
|
||
);
|
||
|
||
} else {
|
||
|
||
buildMixedTrendChart(
|
||
canvasId,
|
||
[],
|
||
[],
|
||
[]
|
||
);
|
||
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| DONUT
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
if (type === 'donut') {
|
||
|
||
const grouped = {};
|
||
|
||
rows.forEach(r => {
|
||
|
||
const pathogen =
|
||
r.pathogen || 'Unknown';
|
||
|
||
grouped[pathogen] ??= 0;
|
||
|
||
grouped[pathogen] +=
|
||
Number(r.total_positive || 0);
|
||
|
||
});
|
||
|
||
const donutRows =
|
||
|
||
Object.entries(grouped)
|
||
|
||
.map(([pathogen, total]) => ({
|
||
pathogen,
|
||
total
|
||
}));
|
||
|
||
buildDistributionChart(
|
||
canvasId,
|
||
'doughnut',
|
||
donutRows,
|
||
'pathogen',
|
||
'total'
|
||
);
|
||
|
||
if (charts[canvasId]) {
|
||
|
||
const totalCases = rows.reduce(
|
||
(sum, r) =>
|
||
sum + Number(r.total_cases || 0),
|
||
0
|
||
);
|
||
|
||
charts[canvasId].$afiTotalCases =
|
||
totalCases;
|
||
|
||
charts[canvasId].update();
|
||
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| YEARLY VIEW
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
const years = [...new Set(
|
||
rows.map(r => Number(r.year))
|
||
)];
|
||
|
||
const totalYears =
|
||
Math.max(...years) - Math.min(...years);
|
||
|
||
const useYearlyView =
|
||
totalYears >= 5;
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| LABELS
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
let labels;
|
||
|
||
if (useYearlyView) {
|
||
|
||
labels = [...new Set(
|
||
rows.map(r => String(r.year))
|
||
)].sort();
|
||
|
||
} else {
|
||
|
||
labels = [...new Set(
|
||
rows.map(r =>
|
||
`${r.year}-W${r.period}`
|
||
)
|
||
)].sort((a, b) => {
|
||
|
||
const [yearA, weekA] =
|
||
a.split('-W').map(Number);
|
||
|
||
const [yearB, weekB] =
|
||
b.split('-W').map(Number);
|
||
|
||
if (yearA !== yearB) {
|
||
return yearA - yearB;
|
||
}
|
||
|
||
return weekA - weekB;
|
||
|
||
});
|
||
|
||
}
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| TOTAL CASES
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
const totalCases = labels.map(label => {
|
||
|
||
if (useYearlyView) {
|
||
|
||
return rows
|
||
.filter(r =>
|
||
String(r.year) === label
|
||
)
|
||
.reduce(
|
||
(sum, r) =>
|
||
sum + Number(r.total_cases || 0),
|
||
0
|
||
);
|
||
|
||
}
|
||
|
||
const row = rows.find(r =>
|
||
`${r.year}-W${r.period}` === label
|
||
);
|
||
|
||
return row?.total_cases || 0;
|
||
|
||
});
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| PATHOGENS
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
const pathogens = [
|
||
|
||
...new Set(
|
||
rows.map(r => r.pathogen)
|
||
)
|
||
|
||
];
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| LINES
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
const lines = pathogens.map(
|
||
(pathogen, i) => ({
|
||
|
||
label: `${pathogen} %`,
|
||
|
||
data: labels.map(label => {
|
||
|
||
if (useYearlyView) {
|
||
|
||
const filtered = rows.filter(r =>
|
||
String(r.year) === label &&
|
||
r.pathogen === pathogen
|
||
);
|
||
|
||
const total =
|
||
filtered.reduce(
|
||
(sum, r) =>
|
||
sum + Number(r.positivity_rate || 0),
|
||
0
|
||
);
|
||
|
||
return filtered.length
|
||
? total / filtered.length
|
||
: 0;
|
||
|
||
}
|
||
|
||
const row = rows.find(r =>
|
||
|
||
`${r.year}-W${r.period}`
|
||
=== label
|
||
|
||
&& r.pathogen === pathogen
|
||
|
||
);
|
||
|
||
return row?.positivity_rate || 0;
|
||
|
||
}),
|
||
|
||
color:
|
||
COLORS[
|
||
i % COLORS.length
|
||
]
|
||
|
||
})
|
||
);
|
||
|
||
buildMixedTrendChart(
|
||
canvasId,
|
||
labels,
|
||
totalCases,
|
||
lines
|
||
);
|
||
|
||
}
|
||
function renderPathogenChart(rows = []) {
|
||
buildDistributionChart(
|
||
'pathogenChart',
|
||
'doughnut',
|
||
rows,
|
||
'pathogen'
|
||
);
|
||
}
|
||
function renderSentinel(rows = []) {
|
||
buildDistributionChart(
|
||
'sentinelChart',
|
||
'pie',
|
||
rows,
|
||
'name'
|
||
);
|
||
}
|
||
function renderSubtypeChart(rows = []) {
|
||
|
||
console.log('renderSubtypeChart');
|
||
|
||
buildDistributionChart(
|
||
'subtypeChart',
|
||
'bar',
|
||
rows,
|
||
'subtype',
|
||
'total',
|
||
getSubtypeColor
|
||
);
|
||
}
|
||
function renderDemographics(data = {}) {
|
||
|
||
buildDistributionChart(
|
||
'ageChart',
|
||
'doughnut',
|
||
data.age_distribution || [],
|
||
'age_group'
|
||
);
|
||
|
||
buildDistributionChart(
|
||
'sexChart',
|
||
'bar',
|
||
data.sex_distribution || [],
|
||
'patient_sex'
|
||
);
|
||
} |