finalized overview page
This commit is contained in:
@@ -74,6 +74,7 @@ class DashboardController extends Controller
|
||||
}
|
||||
|
||||
$data = $this->service->aggregateAllPrograms(
|
||||
|
||||
$range['startYear'],
|
||||
$range['startWeek'],
|
||||
$range['endYear'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
padding: 20,
|
||||
boxWidth: 30,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
bottom: 50
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Total sample'
|
||||
pointStyle: 'circle'
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
position: 'right',
|
||||
grid: {
|
||||
drawOnChartArea: false
|
||||
datalabels: {
|
||||
color: "#282626",
|
||||
anchor: type === "bar" ? "end" : "center",
|
||||
align: type === "bar" ? "top" : "center",
|
||||
font: {
|
||||
size: 10
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: '% Positive'
|
||||
},
|
||||
ticks: {
|
||||
callback: value => value + '%'
|
||||
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
|
||||
}
|
||||
},
|
||||
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: {
|
||||
top: 30,
|
||||
bottom: 20
|
||||
}
|
||||
},
|
||||
|
||||
scales: {
|
||||
y: {
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Cases'
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
position: 'right',
|
||||
grid: { drawOnChartArea: false },
|
||||
title: {
|
||||
display: true,
|
||||
text: '% Positivity'
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// 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 + '%'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
@@ -1,7 +1,6 @@
|
||||
class DashboardFilter {
|
||||
|
||||
constructor(onChange) {
|
||||
|
||||
this.onChange = onChange;
|
||||
|
||||
this.rangeSelect = document.getElementById("trend_range");
|
||||
|
||||
@@ -238,7 +238,7 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
|
||||
})
|
||||
.bindTooltip(`
|
||||
<strong>${province}</strong><br>
|
||||
${programName}<br>
|
||||
${programName}<br>
|
||||
Total: ${row.total}
|
||||
`)
|
||||
.addTo(map);
|
||||
|
||||
@@ -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}<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 renderTrend(valueId, changeId, current, previous, suffix = '') {
|
||||
|
||||
const valueEl = document.getElementById(valueId);
|
||||
@@ -49,16 +159,17 @@ function renderTrend(valueId, changeId, current, previous, suffix = '') {
|
||||
function renderProgramTrend(rows) {
|
||||
|
||||
rows = rows || [];
|
||||
|
||||
const labels = rows.map(r => `W${r.period}`);
|
||||
const samples = rows.map(r => r.total_samples || 0);
|
||||
const positivity = rows.map(r => r.positivity_rate || 0);
|
||||
const fluRate = rows.map(r => r.influenza_rate || 0);
|
||||
const covidRate = rows.map(r => r.covid_rate || 0);
|
||||
|
||||
buildMixedTrendChart(
|
||||
'trendChart',
|
||||
labels,
|
||||
samples,
|
||||
positivity
|
||||
fluRate,
|
||||
covidRate
|
||||
);
|
||||
}
|
||||
function renderSummary(summary) {
|
||||
@@ -117,26 +228,46 @@ function renderSummary(summary) {
|
||||
);
|
||||
}
|
||||
function renderDashboard(data) {
|
||||
console.log("SUMMARY:", data.summary);
|
||||
|
||||
data = data || {};
|
||||
|
||||
|
||||
renderProgramTrend(data.trend || []);
|
||||
renderSummary(data.summary || {});
|
||||
buildChart(
|
||||
'provinceChart',
|
||||
'bar',
|
||||
(data.province_distribution || []).map(r => r.site_province_name),
|
||||
(data.province_distribution || []).map(r => r.total)
|
||||
);
|
||||
renderProvinceHeatmap(data.province_distribution || []);
|
||||
// buildStackedChart(
|
||||
// "pathogenChart",
|
||||
// labels,
|
||||
// [
|
||||
// {
|
||||
// label: "Influenza",
|
||||
// data: influenza,
|
||||
// backgroundColor: "#2E7D32"
|
||||
// },
|
||||
// {
|
||||
// label: "SARS-CoV-2",
|
||||
// data: covid,
|
||||
// backgroundColor: "#A5D6A7"
|
||||
// }
|
||||
// ]
|
||||
// );
|
||||
const pathogenRows = (data.pathogen_distribution || [])
|
||||
.sort((a, b) => b.total - a.total);
|
||||
const colors = [
|
||||
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
|
||||
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16'
|
||||
];
|
||||
|
||||
|
||||
|
||||
buildChart(
|
||||
'pathogenChart',
|
||||
'bar',
|
||||
(data.pathogen_distribution || []).map(r => r.pathogen_name),
|
||||
(data.pathogen_distribution || []).map(r => r.total),
|
||||
'Positive'
|
||||
'doughnut',
|
||||
pathogenRows.map(r => r.pathogen),
|
||||
pathogenRows.map(r => r.total)
|
||||
);
|
||||
charts['pathogenChart'].data.datasets[0].backgroundColor = colors;
|
||||
charts['pathogenChart'].update();
|
||||
|
||||
buildChart(
|
||||
'ageChart',
|
||||
@@ -144,6 +275,8 @@ function renderDashboard(data) {
|
||||
(data.age_distribution || []).map(r => r.age_group),
|
||||
(data.age_distribution || []).map(r => r.total)
|
||||
);
|
||||
charts['ageChart'].data.datasets[0].backgroundColor = colors;
|
||||
charts['ageChart'].update();
|
||||
|
||||
buildChart(
|
||||
'sexChart',
|
||||
@@ -151,5 +284,24 @@ function renderDashboard(data) {
|
||||
(data.sex_distribution || []).map(r => r.patient_sex),
|
||||
(data.sex_distribution || []).map(r => r.total)
|
||||
);
|
||||
charts['sexChart'].data.datasets[0].backgroundColor = colors;
|
||||
charts['sexChart'].update();
|
||||
|
||||
buildChart(
|
||||
'subtypeChart',
|
||||
'bar',
|
||||
(data.subtype_distribution || []).map(r => r.subtype),
|
||||
(data.subtype_distribution || []).map(r => r.total)
|
||||
);
|
||||
charts['subtypeChart'].data.datasets[0].backgroundColor = colors;
|
||||
charts['subtypeChart'].update();
|
||||
buildChart(
|
||||
'sentinelChart',
|
||||
'pie',
|
||||
(data.sentinel_sites || []).map(r => r.name),
|
||||
(data.sentinel_sites || []).map(r => r.total)
|
||||
);
|
||||
charts['sentinelChart'].data.datasets[0].backgroundColor = colors;
|
||||
charts['sentinelChart'].update();
|
||||
|
||||
}
|
||||
|
||||
@@ -50,14 +50,9 @@
|
||||
<div class="col-md-3">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<small>Total Cases Reported (Last 7 Days)</small>
|
||||
<small>Total Cases Reported (latest epiweek)</small>
|
||||
<h3 id="totalCases">0</h3>
|
||||
|
||||
<small id="casesChange" class="text-muted">
|
||||
— No change
|
||||
</small>
|
||||
|
||||
<small id="casesChange" class="text-muted">— No change</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,14 +60,9 @@
|
||||
<div class="col-md-3">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<small>Overall Positivity Rate</small>
|
||||
<h3 id="positivityRate">0%</h3>
|
||||
|
||||
<small id="positivityChange" class="text-muted">
|
||||
— No change
|
||||
</small>
|
||||
|
||||
<small id="positivityChange" class="text-muted">— No change</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,139 +70,152 @@
|
||||
<div class="col-md-3">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<small>Influenza Rate</small>
|
||||
<h3 id="influenzaRate">0%</h3>
|
||||
|
||||
<small id="influenzaChange" class="text-muted">
|
||||
— No change
|
||||
</small>
|
||||
|
||||
<small id="influenzaChange" class="text-muted">— No change</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
|
||||
<small>SARS-Cov-2 Rate</small>
|
||||
<small>SARS-CoV-2 Rate</small>
|
||||
<h3 id="covidRate">0%</h3>
|
||||
<small id="covidChange" class="text-muted">— No change</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small id="covidChange" class="text-muted">
|
||||
— No change
|
||||
</small>
|
||||
</div>
|
||||
<!-- TREND CHART (PRIMARY) -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body" style="height:520px;">
|
||||
|
||||
<h6 class="fw-bold mb-3">
|
||||
Case Trends & Positivity Rate by Epiweek
|
||||
</h6>
|
||||
|
||||
<div style="height:460px; position:relative;">
|
||||
<canvas id="trendChart"></canvas>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PATHOGEN DISTRIBUTION -->
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body" style="height:520px">
|
||||
|
||||
<h6 class="fw-bold mb-3">Pathogen Distribution</h6>
|
||||
|
||||
<div style="height:460px; position:relative;">
|
||||
<canvas id="pathogenChart"></canvas>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- TREND CHART -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body" style="height:500px;">
|
||||
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h6 class="fw-bold">Case Trends and Positivity Rate by Epiweek</h6>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="trendChart"></canvas>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- PROVINCE + PATHOGEN -->
|
||||
<!-- MAP + SITE+subtype -->
|
||||
<div class="row g-3 mb-4">
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
<!-- MAP -->
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body" style="height:300px">
|
||||
<div class="card-body" style="height:520px">
|
||||
|
||||
<h6 class="fw-bold mb-3">
|
||||
Cases by Province
|
||||
</h6>
|
||||
<h6 class="fw-bold mb-3">Cases by Province</h6>
|
||||
|
||||
<canvas id="provinceChart"></canvas>
|
||||
<div id="provinceMap" style="height:450px;"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body" style="height:300px">
|
||||
<div class="card-body" style="height:520px">
|
||||
|
||||
<h6 class="fw-bold mb-3">
|
||||
Pathogen Distribution
|
||||
</h6>
|
||||
<h6 class="fw-bold mb-3">Sentinel Sites & Influenza Subtypes</h6>
|
||||
|
||||
<canvas id="pathogenChart"></canvas>
|
||||
<div class="row ">
|
||||
|
||||
<!-- SENTINEL PIE -->
|
||||
<div class="col-md-6 d-flex flex-column">
|
||||
<small class="text-muted mb-2">Cases by Sentinel Site</small>
|
||||
<div style="height: 460px; position:relative;">
|
||||
<canvas id="sentinelChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SUBTYPE -->
|
||||
<div class="col-md-6 d-flex flex-column">
|
||||
<small class="text-muted mb-2">Influenza Subtypes</small>
|
||||
<div style="height: 460px; position:relative;">
|
||||
<canvas id="subtypeChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- DEMOGRAPHIC -->
|
||||
<!-- DEMOGRAPHICS -->
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body" style="height:300px">
|
||||
<div class="card-body" style="height:400px">
|
||||
<div class="row h-100">
|
||||
<div class="col-md-6 d-flex flex-column">
|
||||
<h6 class="fw-bold mb-3">Age Distribution</h6>
|
||||
|
||||
<h6 class="fw-bold mb-3">Age Distribution</h6>
|
||||
|
||||
<canvas id="ageChart"></canvas>
|
||||
<div style="height:360px; position:relative;">
|
||||
<canvas id="ageChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex flex-column">
|
||||
<h6 class="fw-bold mb-3">Sex Distribution</h6>
|
||||
|
||||
<div style="height:360px; position:relative;">
|
||||
<canvas id="sexChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body" style="height:300px">
|
||||
<div class="card-body" style="height:400px">
|
||||
|
||||
<h6 class="fw-bold mb-3">Sex Distribution</h6>
|
||||
|
||||
<canvas id="sexChart"></canvas>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
|
||||
<script>
|
||||
window.SURVEILLANCE_ID = {{ $selected->id }};
|
||||
window.PROGRAM_CODE = "{{ $selected->code }}";
|
||||
</script>
|
||||
|
||||
<script src="/js/program.js"></script>
|
||||
|
||||
@endsection
|
||||
@@ -51,7 +51,7 @@
|
||||
<div class="mb-3">
|
||||
<h5 class="fw-bold mb-1">Epidemic Trend</h5>
|
||||
<p class="text-muted small mb-0">
|
||||
Weekly reported cases by surveillance program
|
||||
(based on selected epiweek range)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
<head>
|
||||
<title>NRML Dashboard</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="/js/dashboard/filter.js"></script>
|
||||
<script src="/js/dashboard/charts.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
|
||||
<script src="/js/dashboard/filter.js"></script>
|
||||
<script src="/js/dashboard/charts.js"></script>
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -139,7 +142,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
@yield('scripts')
|
||||
@yield('scripts')
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user