diff --git a/dashboard/app/Http/Controllers/Api/DashboardController.php b/dashboard/app/Http/Controllers/Api/DashboardController.php index c963d4d..4f40631 100644 --- a/dashboard/app/Http/Controllers/Api/DashboardController.php +++ b/dashboard/app/Http/Controllers/Api/DashboardController.php @@ -184,7 +184,6 @@ class DashboardController extends Controller public function covidLineageRelativeOverTime(Request $request) { $range = $this->getEpiRange($request); - if (!$range) { return response()->json(['error' => 'Missing epiweek range'], 400); } @@ -202,7 +201,6 @@ class DashboardController extends Controller public function influenzaRelativeOverTime(Request $request) { $range = $this->getEpiRange($request); - if (!$range) { return response()->json(['error' => 'Missing epiweek range'], 400); } @@ -218,6 +216,24 @@ class DashboardController extends Controller } + public function influenzaRelativeOverTimeSequencing(Request $request) + { + $range = $this->getEpiRange($request); + if (!$range) { + return response()->json(['error' => 'Missing epiweek range'], 400); + } + + $data = $this->service->influenzaRelativeOverTimeSequencing( + $range['startYear'], + $range['startWeek'], + $range['endYear'], + $range['endWeek'] + ); + + return response()->json($data); + } + + diff --git a/dashboard/app/Services/DashboardService.php b/dashboard/app/Services/DashboardService.php index 9e8c013..036e851 100644 --- a/dashboard/app/Services/DashboardService.php +++ b/dashboard/app/Services/DashboardService.php @@ -1020,7 +1020,7 @@ class DashboardService }) ->selectRaw(" - surveillance_cases.week_data as period, + concat(surveillance_cases.year_data,'-',surveillance_cases.week_data) as period, subtype, COUNT(DISTINCT surveillance_cases.lab_code) as total ") @@ -1029,4 +1029,34 @@ class DashboardService ->orderBy('period') ->get(); } + + + public function influenzaRelativeOverTimeSequencing($startYear, $startWeek, $endYear, $endWeek) + { + return SurveillanceCase::join('case_lab_results', function ($join) { + $join->on('surveillance_cases.lab_code', '=', 'case_lab_results.lab_code') + ->on('surveillance_cases.surveillance_id', '=', 'case_lab_results.surveillance_id'); + }) + ->where(function ($q) use ($startYear, $startWeek, $endYear, $endWeek) { + $q->whereRaw( + "(surveillance_cases.year_data * 100 + surveillance_cases.week_data) BETWEEN ? AND ?", + [ + $startYear * 100 + $startWeek, + $endYear * 100 + $endWeek + ] + ); + }) + ->whereRaw('case_lab_results.is_positive = 1 and surveillance_cases.surveillance_id in(6) and case_lab_results.indicator="Influenza"') + ->selectRaw(" + case_lab_results.pathogen_name as lineage, + concat(surveillance_cases.year_data,'-',surveillance_cases.week_data) as week, + COUNT(DISTINCT surveillance_cases.lab_code) as total + ") + ->groupBy( + 'case_lab_results.pathogen_name', + 'week' + ) + ->get(); + } + } diff --git a/dashboard/public/js/dashboard/charts.js b/dashboard/public/js/dashboard/charts.js index aaa62d2..7051692 100644 --- a/dashboard/public/js/dashboard/charts.js +++ b/dashboard/public/js/dashboard/charts.js @@ -8,7 +8,7 @@ Chart.register({ (ds.data || []).some(v => Number(v) > 0) ); - chart.$noData = !hasData; + chart.$noData = !hasData; if (hasData) return; @@ -94,7 +94,7 @@ function buildStackedChart(canvasId, labels, datasets) { bottom: 30 } }, - + plugins: { legend: { @@ -159,9 +159,9 @@ function buildChart(id, type, labels, data) { layout: { padding: 30 }, - - indexAxis: isHorizontal ? 'y' : 'x', + + indexAxis: isHorizontal ? 'y' : 'x', plugins: { legend: { @@ -189,7 +189,7 @@ function buildChart(id, type, labels, data) { }, datalabels: { color: "#282626", - offset: 6, + offset: 6, clip: false, display: (ctx) => { const chart = ctx.chart; @@ -257,7 +257,7 @@ function buildChart(id, type, labels, data) { arc: { borderWidth: (ctx) => ctx.chart.$noData ? 0 : 1 } - + }; } @@ -306,7 +306,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) { }, - + { type: 'line', label: 'COVID-19 %', @@ -318,7 +318,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) { pointStyle: 'line', }, - + { type: 'bar', label: 'Total Cases', @@ -327,7 +327,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) { borderRadius: 2, barPercentage: 0.8, categoryPercentage: 0.7, - yAxisID: 'y', + yAxisID: 'y', } ] @@ -338,7 +338,6 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) { options: { responsive: true, maintainAspectRatio: false, - plugins: { legend: { position: 'bottom', @@ -351,6 +350,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) { align: "top", anchor: "end", color: "#555", + display: false, font: { size: 10 }, @@ -387,7 +387,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) { display: true, text: '% Positivity' }, - + } } } @@ -416,7 +416,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) { // pointStyle: 'line', // yAxisID: 'y1', - + // }, // { @@ -428,7 +428,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) { // barPercentage: 0.8, // categoryPercentage: 0.7, // yAxisID: 'y', - + // } // ] // }, @@ -502,4 +502,4 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) { // } // } // }); -// } \ No newline at end of file +// } diff --git a/dashboard/public/js/sequencing.js b/dashboard/public/js/sequencing.js index 6cfa5c8..daac952 100644 --- a/dashboard/public/js/sequencing.js +++ b/dashboard/public/js/sequencing.js @@ -1,20 +1,33 @@ let sequencingChart; +let covidLineageFrequencyChart; +let influenzaSubtypeFrequencyChart; + document.addEventListener("DOMContentLoaded", () => { - const canvas = document.getElementById('sequencingChart'); - if (!canvas) return; + //const canvas = document.getElementById('sequencingChart'); + //if (!canvas) return; 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 => { - renderSequencingChart(data.trend || []); + //renderSequencingChart(data.trend || []); renderSequencingCountChart(data.trend || []); renderSequencingPieChart(data.distribution || []); 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 renderSequencingCountChart(rows) { @@ -39,7 +52,7 @@ function renderSequencingCountChart(rows) { const found = rows.find(r => r.period === w && r.subtype === sub); return found ? found.total : 0; }), - backgroundColor: colors[i % colors.length] + backgroundColor: hexToRGBA(colors[i % colors.length], 0.3)// colors[i % colors.length] })); new Chart(ctx, { @@ -59,6 +72,9 @@ function renderSequencingCountChart(rows) { display: true, position: 'bottom', align: 'center' + }, + datalabels: { + display: true } } @@ -123,9 +139,13 @@ function renderSequencingTotalChart(rows) { options: { maintainAspectRatio: false, plugins: { - legend: { display: false } + legend: { display: false }, + datalabels: { + display: false + } }, + } }); } @@ -176,6 +196,8 @@ function renderSequencingChart(rows) { } }); + console.log('aggregated', aggregated) + const cleanRows = Object.values(aggregated); const weeks = [...new Set(cleanRows.map(r => r.period))]; @@ -235,4 +257,310 @@ function renderSequencingChart(rows) { } } }); -} \ No newline at end of file +} + + + +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 => { + + // Extract unique weeks (X-axis) + const weeks = [...new Set(data.map(item => item.week))].sort(); + + // Extract unique lineages + const lineages = [...new Set(data.map(item => item.lineage))]; + + // Color palette + const colors = [ + '#84cc16','#22c55e','#06b6d4','#3b82f6', + '#6366f1','#a855f7','#ec4899','#ef4444', + '#f97316','#eab308' + ]; + + // Build datasets + 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, // area fill + tension: 0.4, // smooth curve + borderColor: 'transparent', // hide the line + borderWidth: 0, + pointRadius: 0, // hide points + backgroundColor: hexToRGBA(colors[index % colors.length], 0.3), + stack: 'total' + }; + }); + + // Destroy previous chart if exists + if (covidLineageFrequencyChart) covidLineageFrequencyChart.destroy(); + + const ctx = document.getElementById('covidLineageFrequency').getContext('2d'); + + 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' + }, + } + } + } + }); + + // ------------------------- + // Custom right-side scrollable legend + // ------------------------- + const legendContainer = document.getElementById('legendContainer'); + legendContainer.innerHTML = ''; // clear old legend + + 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 = ` + + ${dataset.label} + `; + + item.addEventListener('click', () => { + const meta = covidLineageFrequencyChart.getDatasetMeta(index); + + // If the clicked dataset is already the only visible one, show all + const allHidden = datasets.every((d, i) => covidLineageFrequencyChart.getDatasetMeta(i).hidden || i === index); + if (!allHidden) { + // Hide all datasets + datasets.forEach((d, i) => { + covidLineageFrequencyChart.getDatasetMeta(i).hidden = true; + }); + // Show only clicked + meta.hidden = false; + } else { + // Show all datasets + datasets.forEach((d, i) => { + covidLineageFrequencyChart.getDatasetMeta(i).hidden = false; + }); + } + + covidLineageFrequencyChart.update(); + + // Update legend opacity + Array.from(legendContainer.children).forEach((child, i) => { + const metaItem = covidLineageFrequencyChart.getDatasetMeta(i); + child.style.opacity = metaItem.hidden ? 0.5 : 1; + }); + }); + + legendContainer.appendChild(item); + }); + + // Scrollable CSS (in case legend is long) + 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 => { + + // Extract unique weeks (X-axis) + const weeks = [...new Set(data.map(item => item.week))].sort(); + + const colors = [ + '#84cc16','#22c55e','#06b6d4','#3b82f6', + '#6366f1','#a855f7','#ec4899','#ef4444', + '#f97316','#eab308' + ]; + + // Extract unique lineages + const lineages = [...new Set(data.map(item => item.lineage))]; + // Build datasets + 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, // area fill + tension: 0.4, // smooth curve + borderColor: 'transparent', // hide the line + borderWidth: 0, + pointRadius: 0, // hide points + backgroundColor: hexToRGBA(colors[(index*2) % colors.length], 0.8), + stack: 'total' + }; + }); + + // Destroy previous chart if exists + if (influenzaSubtypeFrequencyChart) influenzaSubtypeFrequencyChart.destroy(); + + const ctx = document.getElementById('influenzaSubtypeFrequency').getContext('2d'); + + 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 // 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' + }, + } + } + } + }); + + const legendContainer = document.getElementById('legendContainerInfluenzaSubtypeFrequency'); + legendContainer.innerHTML = ''; // clear old legend + + 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 = ` + + ${dataset.label} + `; + + item.addEventListener('click', () => { + const meta = influenzaSubtypeFrequencyChart.getDatasetMeta(index); + + // If the clicked dataset is already the only visible one, show all + const allHidden = datasets.every((d, i) => influenzaSubtypeFrequencyChart.getDatasetMeta(i).hidden || i === index); + if (!allHidden) { + // Hide all datasets + datasets.forEach((d, i) => { + influenzaSubtypeFrequencyChart.getDatasetMeta(i).hidden = true; + }); + // Show only clicked + meta.hidden = false; + } else { + // Show all datasets + datasets.forEach((d, i) => { + influenzaSubtypeFrequencyChart.getDatasetMeta(i).hidden = false; + }); + } + + influenzaSubtypeFrequencyChart.update(); + + // Update legend opacity + Array.from(legendContainer.children).forEach((child, i) => { + const metaItem = influenzaSubtypeFrequencyChart.getDatasetMeta(i); + child.style.opacity = metaItem.hidden ? 0.5 : 1; + }); + }); + + legendContainer.appendChild(item); + }); + + // Scrollable CSS (in case legend is long) + legendContainer.style.maxHeight = '375px'; + legendContainer.style.overflowY = 'auto'; + legendContainer.style.padding = '8px'; + legendContainer.style.borderRadius = '0px'; + }); +} diff --git a/dashboard/resources/views/dashboard/overview.blade.php b/dashboard/resources/views/dashboard/overview.blade.php index 037d1f5..cd41f0d 100644 --- a/dashboard/resources/views/dashboard/overview.blade.php +++ b/dashboard/resources/views/dashboard/overview.blade.php @@ -48,7 +48,6 @@
-
Epidemic Trend

(based on selected epiweek range) @@ -109,11 +108,11 @@

(based on selected epiweek range)

- +
(based on selected epiweek range)

- +
-
- +
+ +
+
+
+
Total Sequenced Samples Over Time
+

+ (based on selected epiweek range) +

+ +
+
+ +
+
+
+
Influenza Subtypes Relative Frequencies Over Time
+

+ (based on selected epiweek range) +

+ +
+
+
+
+
-
+
-
-
Lineage Counts Over Time
- +
+
SARS-CoV-2 Lineage/Sublineage Relative Frequencies Over Time
+

+ (based on selected epiweek range) +

+{{-- --}} + +
+{{--
--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- --}} +{{--
--}} +{{--
--}} +{{--
--}} + -
-
-
-
Total Sequenced Samples Over Time
- -
-
-
+
@@ -62,8 +105,8 @@ @section('scripts') -@endsection \ No newline at end of file +@endsection diff --git a/dashboard/resources/views/layouts/app.blade.php b/dashboard/resources/views/layouts/app.blade.php index 8e621e9..163044f 100644 --- a/dashboard/resources/views/layouts/app.blade.php +++ b/dashboard/resources/views/layouts/app.blade.php @@ -46,6 +46,9 @@ top: 0; z-index: 1000; } + .btn{ + border-radius: 0 !important; + } .btn-theme-outline { background-color: #fff; @@ -263,4 +266,4 @@ - \ No newline at end of file + diff --git a/dashboard/routes/api.php b/dashboard/routes/api.php index fafcf38..0210d77 100644 --- a/dashboard/routes/api.php +++ b/dashboard/routes/api.php @@ -11,6 +11,7 @@ Route::get('/dashboard/influenza-subtype-distribution', [DashboardController::cl Route::get('/dashboard/covid-distributed-by-age-group', [DashboardController::class, 'covidDistributedByAgeGroup']); Route::get('/dashboard/covid-lineage-frequency', [DashboardController::class, 'covidLineageRelativeOverTime']); Route::get('/dashboard/influenza-relative-frequency', [DashboardController::class, 'influenzaRelativeOverTime']); +Route::get('/dashboard/influenza-relative-frequency-sequencing', [DashboardController::class, 'influenzaRelativeOverTimeSequencing']); Route::get('/dashboard/sentinel-map', [DashboardController::class, 'sentinelMap']); Route::get('/dashboard/reload', [DashboardController::class, 'fetchSourceData']); Route::get('/dashboard/sequencing', [DashboardController::class, 'sequencing']);