diff --git a/dashboard/app/Http/Controllers/Api/DashboardController.php b/dashboard/app/Http/Controllers/Api/DashboardController.php
index 3b960a3..bc07fb9 100644
--- a/dashboard/app/Http/Controllers/Api/DashboardController.php
+++ b/dashboard/app/Http/Controllers/Api/DashboardController.php
@@ -74,6 +74,7 @@ class DashboardController extends Controller
}
$data = $this->service->aggregateAllPrograms(
+
$range['startYear'],
$range['startWeek'],
$range['endYear'],
diff --git a/dashboard/app/Services/DashboardService.php b/dashboard/app/Services/DashboardService.php
index c00720b..bcd085b 100644
--- a/dashboard/app/Services/DashboardService.php
+++ b/dashboard/app/Services/DashboardService.php
@@ -8,6 +8,16 @@ use App\Models\CaseLabResult;
class DashboardService
{
+ private function totalTested($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
+ {
+ return SurveillanceCase::where('surveillance_id', $surveillanceId)
+ ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
+ $q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
+ ->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
+ })
+ ->distinct('lab_code')
+ ->count('lab_code');
+ }
/*
|--------------------------------------------------------------------------
@@ -56,9 +66,8 @@ class DashboardService
|--------------------------------------------------------------------------
*/
- public function sariSummaryFast($surveillanceId, $year, $week)
+ public function programSummaryFast($surveillanceId, $year, $week)
{
-
$row = SurveillanceCase::leftJoin(
'case_lab_results',
'surveillance_cases.lab_code',
@@ -79,14 +88,20 @@ class DashboardService
END) as overall_positive,
COUNT(DISTINCT CASE
- WHEN case_lab_results.indicator = 'SARI Influenza Test'
- AND case_lab_results.is_positive = 1
+ WHEN case_lab_results.is_positive = 1
+ AND (
+ LOWER(case_lab_results.pathogen_name) LIKE '%influenza%'
+ OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%'
+ )
THEN surveillance_cases.lab_code
END) as influenza_positive,
COUNT(DISTINCT CASE
- WHEN case_lab_results.indicator = 'SARI Covid Test'
- AND case_lab_results.is_positive = 1
+ WHEN case_lab_results.is_positive = 1
+ AND (
+ LOWER(case_lab_results.pathogen_name) LIKE '%covid%'
+ OR LOWER(case_lab_results.pathogen_name) LIKE '%sars%'
+ )
THEN surveillance_cases.lab_code
END) as covid_positive
")
@@ -103,29 +118,14 @@ class DashboardService
];
}
-
return [
-
'cases' => $row->total_cases,
- 'overall_rate' => round(
- ($row->overall_positive / $row->total_cases) * 100
- ,
- 1
- ),
+ 'overall_rate' => round(($row->overall_positive / $row->total_cases) * 100, 1),
- 'influenza_rate' => round(
- ($row->influenza_positive / $row->total_cases) * 100
- ,
- 1
- ),
-
- 'covid_rate' => round(
- ($row->covid_positive / $row->total_cases) * 100
- ,
- 1
- ),
+ 'influenza_rate' => round(($row->influenza_positive / $row->total_cases) * 100, 1),
+ 'covid_rate' => round(($row->covid_positive / $row->total_cases) * 100, 1),
];
}
@@ -148,9 +148,25 @@ class DashboardService
$prevYear--;
}
- $current = $this->sariSummaryFast($surveillanceId, $endYear, $endWeek);
- $previous = $this->sariSummaryFast($surveillanceId, $prevYear, $prevWeek);
+ $latest = SurveillanceCase::where('surveillance_id', $surveillanceId)
+ ->selectRaw("year_data, week_data")
+ ->orderByDesc('year_data')
+ ->orderByDesc('week_data')
+ ->first();
+ $year = $latest->year_data;
+ $week = $latest->week_data;
+ $current = $this->programSummaryFast($surveillanceId, $year, $week);
+ $previous = $this->programSummaryFast($surveillanceId, $prevYear, $prevWeek);
+ $prevWeek = $week - 1;
+ $prevYear = $year;
+
+ if ($prevWeek <= 0) {
+ $prevWeek = 52;
+ $prevYear--;
+ }
+
+ $previous = $this->programSummaryFast($surveillanceId, $prevYear, $prevWeek);
return [
@@ -195,7 +211,7 @@ class DashboardService
|--------------------------------------------------------------------------
*/
- public function aggregateAllPrograms($periodType, $startYear, $startWeek, $endYear, $endWeek)
+ public function aggregateAllPrograms($startYear, $startWeek, $endYear, $endWeek)
{
$data = SurveillanceCase::selectRaw("
@@ -293,13 +309,34 @@ class DashboardService
$endWeek
),
- 'province_distribution' => $this->provinceCirclesProgram(
+ 'province_distribution' => $this->provinceProgram(
$surveillanceId,
$startYear,
$startWeek,
$endYear,
$endWeek
- )
+ ),
+ 'virus_trend' => $this->virusTrend(
+ $surveillanceId,
+ $startYear,
+ $startWeek,
+ $endYear,
+ $endWeek
+ ),
+ 'subtype_distribution' => $this->subtypeDistribution(
+ $surveillanceId,
+ $startYear,
+ $startWeek,
+ $endYear,
+ $endWeek
+ ),
+ 'sentinel_sites' => $this->sentinelSites(
+ $surveillanceId,
+ $startYear,
+ $startWeek,
+ $endYear,
+ $endWeek
+ ),
];
@@ -340,14 +377,46 @@ class DashboardService
})
->selectRaw("
- surveillance_cases.year_data as year,
- surveillance_cases.week_data as period,
- COUNT(DISTINCT surveillance_cases.lab_code) as total_samples,
+ surveillance_cases.year_data as year,
+ surveillance_cases.week_data as period,
- ROUND(
- SUM(CASE WHEN case_lab_results.is_positive = 1 THEN 1 ELSE 0 END)
- / NULLIF(COUNT(*),0) * 100,1
- ) as positivity_rate
+ COUNT(DISTINCT surveillance_cases.lab_code) as total_samples,
+
+ -- Overall positivity rate
+ ROUND(
+ COUNT(DISTINCT CASE
+ WHEN case_lab_results.is_positive = 1
+ THEN surveillance_cases.lab_code
+ END)
+ / NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
+ ,1) as positivity_rate,
+
+ -- Influenza positivity rate
+ ROUND(
+ COUNT(DISTINCT CASE
+ WHEN case_lab_results.is_positive = 1
+ AND (
+ LOWER(case_lab_results.pathogen_name) LIKE '%influenza%'
+ OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%'
+ )
+ THEN surveillance_cases.lab_code
+ END)
+ / NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
+ ,1) as influenza_rate,
+
+ -- COVID positivity rate
+ ROUND(
+ COUNT(DISTINCT CASE
+ WHEN case_lab_results.is_positive = 1
+ AND (
+ case_lab_results.pathogen_name = 'Positive'
+ OR case_lab_results.pathogen_name = 'SARS-CoV-2'
+ )
+ AND case_lab_results.indicator LIKE '%Covid%'
+ THEN surveillance_cases.lab_code
+ END)
+ / NULLIF(COUNT(DISTINCT surveillance_cases.lab_code), 0) * 100
+ ,1) as covid_rate
")
->groupBy(
@@ -399,7 +468,49 @@ class DashboardService
return $results;
}
+ public function virusTrend($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
+ {
+ return CaseLabResult::join(
+ 'surveillance_cases',
+ 'case_lab_results.lab_code',
+ '=',
+ 'surveillance_cases.lab_code'
+ )
+ ->where('surveillance_cases.surveillance_id', $surveillanceId)
+
+ ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
+ $q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
+ ->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
+ })
+
+ ->selectRaw("
+ surveillance_cases.week_data as period,
+
+ COUNT(DISTINCT CASE
+ WHEN case_lab_results.is_positive = 1
+ AND (
+ LOWER(case_lab_results.pathogen_name) LIKE '%influenza%'
+ OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%'
+ )
+ THEN surveillance_cases.lab_code
+ END) as influenza,
+
+ COUNT(DISTINCT CASE
+ WHEN case_lab_results.is_positive = 1
+ AND (
+ case_lab_results.pathogen_name = 'Positive'
+ OR case_lab_results.pathogen_name = 'SARS-CoV-2'
+ )
+ AND case_lab_results.indicator LIKE '%Covid%'
+ THEN surveillance_cases.lab_code
+ END) as covid
+ ")
+
+ ->groupBy('surveillance_cases.week_data')
+ ->orderBy('surveillance_cases.week_data')
+ ->get();
+ }
/*
@@ -407,14 +518,24 @@ class DashboardService
| Province Distribution (Program)
|--------------------------------------------------------------------------
*/
-
- public function provinceCirclesProgram($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
+ public function provinceCircles($startYear, $startWeek, $endYear, $endWeek)
+ {
+ return SurveillanceCase::selectRaw(" surveillance_cases.site_province_name, surveillance_cases.surveillance_id, COUNT(*) as total ")->join('case_lab_results', 'surveillance_cases.lab_code', '=', 'case_lab_results.lab_code')->where('case_lab_results.is_positive', 1)->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
+ $q->whereRaw("(surveillance_cases.year_data > ? OR (surveillance_cases.year_data = ? AND surveillance_cases.week_data >= ?))", [$startYear, $startYear, $startWeek])->whereRaw("(surveillance_cases.year_data < ? OR (surveillance_cases.year_data = ? AND surveillance_cases.week_data <= ?))", [$endYear, $endYear, $endWeek]);
+ })->groupBy('surveillance_cases.site_province_name', 'surveillance_cases.surveillance_id')->get();
+ }
+ public function provinceProgram($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
{
-
return SurveillanceCase::selectRaw("
- surveillance_cases.site_province_name,
- COUNT(DISTINCT surveillance_cases.lab_code) as total
- ")
+ surveillance_cases.site_province_name,
+
+ COUNT(DISTINCT surveillance_cases.lab_code) as total,
+
+ COUNT(DISTINCT CASE
+ WHEN case_lab_results.is_positive = 1
+ THEN surveillance_cases.lab_code
+ END) as positive
+ ")
->join(
'case_lab_results',
@@ -424,7 +545,6 @@ class DashboardService
)
->where('surveillance_cases.surveillance_id', $surveillanceId)
- ->where('case_lab_results.is_positive', 1)
->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
@@ -442,10 +562,30 @@ class DashboardService
->groupBy('surveillance_cases.site_province_name')
->get();
-
}
+ /*
+ |--------------------------------------------------------------------------
+ | sentinel sites
+ |--------------------------------------------------------------------------
+ */
+ public function sentinelSites($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
+ {
+ return SurveillanceCase::selectRaw("
+ sentinel_site_name as name,
+ COUNT(DISTINCT lab_code) as total
+ ")
+ ->where('surveillance_id', $surveillanceId)
+ ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
+ $q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
+ ->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
+ })
+
+ ->groupBy('sentinel_site_name')
+ ->orderByDesc('total') // nice for chart
+ ->get();
+ }
/*
|--------------------------------------------------------------------------
@@ -455,44 +595,84 @@ class DashboardService
public function pathogenDistribution($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
{
+ $total = $this->totalTested($surveillanceId, $startYear, $startWeek, $endYear, $endWeek);
- return CaseLabResult::selectRaw("
- pathogen_name,
- COUNT(*) as total
- ")
+ $rows = CaseLabResult::join(
+ 'surveillance_cases',
+ 'case_lab_results.lab_code',
+ '=',
+ 'surveillance_cases.lab_code'
+ )
+ ->where('surveillance_cases.surveillance_id', $surveillanceId)
- ->join(
- 'surveillance_cases',
- 'case_lab_results.lab_code',
- '=',
- 'surveillance_cases.lab_code'
- )
+ ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
+ $q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
+ ->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
+ })
+ ->where('case_lab_results.is_positive', 1)
+
+ ->selectRaw("
+ CASE
+ WHEN LOWER(case_lab_results.pathogen_name) LIKE '%influenza%'
+ OR LOWER(case_lab_results.pathogen_name) LIKE '%influzena%'
+ THEN 'Influenza'
+
+ WHEN case_lab_results.pathogen_name = 'Positive'
+ AND case_lab_results.indicator LIKE '%Covid%'
+ THEN 'SARS-CoV-2'
+
+ ELSE case_lab_results.pathogen_name
+ END as pathogen,
+
+ COUNT(DISTINCT surveillance_cases.lab_code) as total
+ ")
+
+ ->groupBy('pathogen')
+ ->havingRaw("pathogen IS NOT NULL AND pathogen != ''")
+ ->orderByDesc('total')
+ ->get();
+
+ return $rows->map(function ($r) use ($total) {
+ $r->rate = $total > 0 ? round(($r->total / $total) * 100, 1) : 0;
+ return $r;
+ });
+ }
+ public function subtypeDistribution($surveillanceId, $startYear, $startWeek, $endYear, $endWeek)
+ {
+ $total = $this->totalTested($surveillanceId, $startYear, $startWeek, $endYear, $endWeek);
+
+ $rows = CaseLabResult::join(
+ 'surveillance_cases',
+ 'case_lab_results.lab_code',
+ '=',
+ 'surveillance_cases.lab_code'
+ )
->where('surveillance_cases.surveillance_id', $surveillanceId)
->where('case_lab_results.is_positive', 1)
->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) {
-
- $q->whereRaw(
- "(year_data > ? OR (year_data = ? AND week_data >= ?))",
- [$startYear, $startYear, $startWeek]
- )
-
- ->whereRaw(
- "(year_data < ? OR (year_data = ? AND week_data <= ?))",
- [$endYear, $endYear, $endWeek]
- );
-
+ $q->whereRaw("(year_data > ? OR (year_data = ? AND week_data >= ?))", [$startYear, $startYear, $startWeek])
+ ->whereRaw("(year_data < ? OR (year_data = ? AND week_data <= ?))", [$endYear, $endYear, $endWeek]);
})
- ->groupBy('pathogen_name')
- ->orderByDesc('total')
- ->get();
+ ->selectRaw("
+ subtype,
+ COUNT(DISTINCT surveillance_cases.lab_code) as total
+ ")
+ ->groupBy('subtype')
+ ->havingRaw("subtype IS NOT NULL AND subtype != 'Positive' AND subtype != ''")
+ ->orderByDesc('total')
+ ->get();
+
+ return $rows->map(function ($r) use ($total) {
+ $r->rate = $total > 0 ? round(($r->total / $total) * 100, 1) : 0;
+ return $r;
+ });
}
-
/*
|--------------------------------------------------------------------------
| Age Distribution
diff --git a/dashboard/public/js/dashboard/charts.js b/dashboard/public/js/dashboard/charts.js
index ce7b4fd..706b45f 100644
--- a/dashboard/public/js/dashboard/charts.js
+++ b/dashboard/public/js/dashboard/charts.js
@@ -1,6 +1,83 @@
+Chart.register(ChartDataLabels);
const charts = {};
-function buildChart(id, type, labels, data, label = 'Cases') {
+
+function buildStackedChart(canvasId, labels, datasets) {
+ 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: datasets,
+ datalabels: {
+ display: true
+ }
+ },
+
+ 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;
+ }
+ }
+ },
+
+ scales: {
+ x: {
+ stacked: true
+ },
+ y: {
+ stacked: true,
+ beginAtZero: true
+ }
+ }
+ }
+ });
+}
+function buildChart(id, type, labels, data) {
const ctx = document.getElementById(id);
@@ -13,96 +90,276 @@ function buildChart(id, type, labels, data, label = 'Cases') {
data: {
labels: labels,
datasets: [{
- label: label,
data: data,
borderWidth: 2,
- tension: 0.3
+ tension: 0.3,
+ barPercentage: 0.8,
+ categoryPercentage: 0.6,
+ maxBarThickness: 50
}]
},
- options: {
- responsive: true,
- maintainAspectRatio: false
- }
- });
-
-}
-function buildMixedTrendChart(canvasId, labels, samples, positivity) {
- const ctx = document.getElementById(canvasId);
- if (!ctx) return;
- if (charts[canvasId]) charts[canvasId].destroy();
- charts[canvasId] = new Chart(ctx, {
- data: {
- labels: labels,
- datasets: [
- {
- type: 'line',
- label: '% Positive',
- data: positivity,
- borderColor: '#1e6ef2',
- borderWidth: 2,
- tension: 0.4,
- fill: false,
- pointRadius: 4,
- pointStyle: 'line',
- yAxisID: 'y1'
- },
- {
- type: 'bar',
- label: 'Total sample',
- data: samples,
- backgroundColor: '#2ecc71',
- borderRadius: 6,
- barPercentage: 0.6,
- pointStyle: 'rect',
- categoryPercentage: 0.7,
- yAxisID: 'y',
- },
- ]
- },
options: {
responsive: true,
maintainAspectRatio: false,
+
+ layout: {
+ padding: {
+ top: 30,
+ bottom: 30
+ }
+ },
+
plugins: {
legend: {
position: 'bottom',
align: 'center',
+ display: type === 'pie' || type === 'doughnut',
+ labels: {
+ padding: 10,
+ boxWidth: 14,
+ boxHeight: 14,
+ usePointStyle: true,
+ pointStyle: 'circle'
+ }
+ },
+ datalabels: {
+ color: "#282626",
+ anchor: type === "bar" ? "end" : "center",
+ align: type === "bar" ? "top" : "center",
+ font: {
+ size: 10
+ },
+ formatter: function(value, ctx) {
+ const data = ctx.chart.data.datasets[0].data;
+ const total = data.reduce((a, b) => a + b, 0);
+ const percent = total ? (value / total * 100).toFixed(1) : 0;
+ return percent + '%';
+ }
+ }
+ }
+ }
+ });
+
+}
+function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
+
+ const ctx = document.getElementById(canvasId);
+ if (!ctx) return;
+
+ if (charts[canvasId]) charts[canvasId].destroy();
+
+ charts[canvasId] = new Chart(ctx, {
+
+ data: {
+ labels: labels,
+ datasets: [
+
+ {
+ type: 'line',
+ label: 'Influenza %',
+ data: fluRate,
+ borderColor: '#fa2929',
+ backgroundColor: '#fa2929',
+ tension: 0.4,
+ yAxisID: 'y1',
+ pointStyle: 'line',
+
+ },
+
+
+ {
+ type: 'line',
+ label: 'COVID-19 %',
+ data: covidRate,
+ borderColor: '#1976D2',
+ backgroundColor: '#1976D2',
+ tension: 0.4,
+ yAxisID: 'y1',
+ pointStyle: 'line',
+ },
+
+
+ {
+ type: 'bar',
+ label: 'Total Cases',
+ data: samples,
+ backgroundColor: '#0B8F3C',
+ borderRadius: 2,
+ barPercentage: 0.8,
+ categoryPercentage: 0.7,
+ yAxisID: 'y',
+ }
+
+ ]
+ },
+
+ plugins: [ChartDataLabels],
+
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+
+ plugins: {
+ legend: {
+ position: 'bottom',
labels: {
usePointStyle: true,
- padding: 20,
- boxWidth: 30,
- font: {
- size: 12
+ padding: 20
+ }
+ },
+ datalabels: {
+ align: "top",
+ anchor: "end",
+ color: "#555",
+ font: {
+ size: 10
+ },
+ formatter: function (value, context) {
+ if (Number(value) === 0) return null;
+
+ if (context.dataset.type === 'line') {
+ return value + '%';
}
+
+ return value;
}
}
},
layout: {
padding: {
- bottom: 50
+ top: 30,
+ bottom: 20
}
},
+
scales: {
y: {
position: 'left',
title: {
display: true,
- text: 'Total sample'
+ text: 'Cases'
}
},
y1: {
position: 'right',
- grid: {
- drawOnChartArea: false
- },
+ grid: { drawOnChartArea: false },
title: {
display: true,
- text: '% Positive'
+ text: '% Positivity'
},
- ticks: {
- callback: value => value + '%'
- }
+
}
}
}
});
-}
\ No newline at end of file
+}
+// function buildMixedTrendChart(canvasId, labels, samples, positivity) {
+
+// const ctx = document.getElementById(canvasId);
+// if (!ctx) return;
+
+// if (charts[canvasId]) charts[canvasId].destroy();
+
+// charts[canvasId] = new Chart(ctx, {
+// data: {
+// labels: labels,
+// datasets: [
+// {
+// type: 'line',
+// label: '% Positive',
+// data: positivity,
+// borderColor: '#1e6ef2',
+// borderWidth: 2,
+// tension: 0.4,
+// fill: false,
+// pointRadius: 2,
+// pointStyle: 'line',
+// yAxisID: 'y1',
+
+
+// },
+
+// {
+// type: 'bar',
+// label: 'Total sample ',
+// data: samples,
+// backgroundColor: '#2ecc71',
+// borderRadius: 2,
+// barPercentage: 0.8,
+// categoryPercentage: 0.7,
+// yAxisID: 'y',
+
+// }
+// ]
+// },
+
+// plugins: [ChartDataLabels],
+
+// options: {
+// responsive: true,
+// maintainAspectRatio: false,
+
+// plugins: {
+// legend: {
+// position: 'bottom',
+// align: 'center',
+// labels: {
+// usePointStyle: true,
+// padding: 20,
+// boxWidth: 30,
+// font: { size: 12 }
+// }
+// },
+// datalabels: {
+// align: "top",
+// anchor: "end",
+// color: "#555",
+// font: {
+// size: 10
+// },
+// formatter: function (value, context) {
+
+// if (Number(value) === 0) return null;
+// if (context.dataset.type === 'line') {
+// console.log(value);
+// return value + '%';
+// }
+
+// return value;
+// }
+// }
+// },
+
+// layout: {
+// padding: {
+// top: 20,
+// bottom: 50
+// }
+// },
+
+// scales: {
+// y: {
+// position: 'left',
+// title: {
+// display: true,
+// text: 'Total sample'
+// }
+// },
+
+// y1: {
+// position: 'right',
+// grid: {
+// drawOnChartArea: false
+// },
+// title: {
+// display: true,
+// text: '% Positive'
+// },
+// ticks: {
+// callback: value => value + '%'
+// }
+// }
+// }
+// }
+// });
+// }
\ No newline at end of file
diff --git a/dashboard/public/js/dashboard/filter.js b/dashboard/public/js/dashboard/filter.js
index 6951a9a..2d12fa3 100644
--- a/dashboard/public/js/dashboard/filter.js
+++ b/dashboard/public/js/dashboard/filter.js
@@ -1,7 +1,6 @@
class DashboardFilter {
constructor(onChange) {
-
this.onChange = onChange;
this.rangeSelect = document.getElementById("trend_range");
diff --git a/dashboard/public/js/overview.js b/dashboard/public/js/overview.js
index ceb29db..8dd0d57 100644
--- a/dashboard/public/js/overview.js
+++ b/dashboard/public/js/overview.js
@@ -238,7 +238,7 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
})
.bindTooltip(`
${province}
- ${programName}
+ ${programName}
Total: ${row.total}
`)
.addTo(map);
diff --git a/dashboard/public/js/program.js b/dashboard/public/js/program.js
index d815dcb..9627be9 100644
--- a/dashboard/public/js/program.js
+++ b/dashboard/public/js/program.js
@@ -1,6 +1,7 @@
const standardPrograms = ['SARI', 'ILI', 'LBM'];
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
-
+let map;
+let provinceLayer;
document.addEventListener("DOMContentLoaded", () => {
@@ -15,6 +16,115 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
+
+
+function renderProvinceHeatmap(rows) {
+
+ const totals = {};
+ rows.forEach(r => {
+ totals[r.site_province_name] = {
+ total: Number(r.total),
+ positive: Number(r.positive)
+ };
+ });
+
+ 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 => {
+
+ function getColor(value) {
+ if (value > 50) return "#b91c1c";
+ if (value >= 10) return "#ef4444";
+ if (value > 0) return "#fecaca";
+ return "#f3f4f600";
+ }
+
+ provinceLayer = L.geoJSON(geo, {
+ style: feature => {
+
+ const province = feature.properties.ADM1_EN;
+ const value = totals[province]?.total || 0;
+
+ return {
+ color: "#444",
+ weight: 1,
+ fillColor: getColor(value),
+ fillOpacity: 0.7
+ };
+ },
+ 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;
+ console.log(province, total, positive, percent);
+
+ layer.bindTooltip(`
+ ${province}
+ Total: ${total}
+ 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 = `
+
- Weekly reported cases by surveillance program + (based on selected epiweek range)