modified Sequencing Page

This commit is contained in:
pcalengratha
2026-04-20 16:02:08 +07:00
parent 90865276e1
commit b021f4c2ba
8 changed files with 465 additions and 45 deletions

View File

@@ -184,7 +184,6 @@ class DashboardController extends Controller
public function covidLineageRelativeOverTime(Request $request) public function covidLineageRelativeOverTime(Request $request)
{ {
$range = $this->getEpiRange($request); $range = $this->getEpiRange($request);
if (!$range) { if (!$range) {
return response()->json(['error' => 'Missing epiweek range'], 400); return response()->json(['error' => 'Missing epiweek range'], 400);
} }
@@ -202,7 +201,6 @@ class DashboardController extends Controller
public function influenzaRelativeOverTime(Request $request) public function influenzaRelativeOverTime(Request $request)
{ {
$range = $this->getEpiRange($request); $range = $this->getEpiRange($request);
if (!$range) { if (!$range) {
return response()->json(['error' => 'Missing epiweek range'], 400); 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);
}

View File

@@ -1020,7 +1020,7 @@ class DashboardService
}) })
->selectRaw(" ->selectRaw("
surveillance_cases.week_data as period, concat(surveillance_cases.year_data,'-',surveillance_cases.week_data) as period,
subtype, subtype,
COUNT(DISTINCT surveillance_cases.lab_code) as total COUNT(DISTINCT surveillance_cases.lab_code) as total
") ")
@@ -1029,4 +1029,34 @@ class DashboardService
->orderBy('period') ->orderBy('period')
->get(); ->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();
}
} }

View File

@@ -338,7 +338,6 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { legend: {
position: 'bottom', position: 'bottom',
@@ -351,6 +350,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
align: "top", align: "top",
anchor: "end", anchor: "end",
color: "#555", color: "#555",
display: false,
font: { font: {
size: 10 size: 10
}, },

View File

@@ -1,22 +1,35 @@
let sequencingChart; let sequencingChart;
let covidLineageFrequencyChart;
let influenzaSubtypeFrequencyChart;
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const canvas = document.getElementById('sequencingChart'); //const canvas = document.getElementById('sequencingChart');
if (!canvas) return; //if (!canvas) return;
new DashboardFilter((startYear, startWeek, endYear, endWeek) => { 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}`) 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(res => res.json())
.then(data => { .then(data => {
renderSequencingChart(data.trend || []); //renderSequencingChart(data.trend || []);
renderSequencingCountChart(data.trend || []); renderSequencingCountChart(data.trend || []);
renderSequencingPieChart(data.distribution || []); renderSequencingPieChart(data.distribution || []);
renderSequencingTotalChart(data.trend || []); 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) { function renderSequencingCountChart(rows) {
const ctx = document.getElementById('sequencingCountChart'); const ctx = document.getElementById('sequencingCountChart');
@@ -39,7 +52,7 @@ function renderSequencingCountChart(rows) {
const found = rows.find(r => r.period === w && r.subtype === sub); const found = rows.find(r => r.period === w && r.subtype === sub);
return found ? found.total : 0; 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, { new Chart(ctx, {
@@ -59,6 +72,9 @@ function renderSequencingCountChart(rows) {
display: true, display: true,
position: 'bottom', position: 'bottom',
align: 'center' align: 'center'
},
datalabels: {
display: true
} }
} }
@@ -123,9 +139,13 @@ function renderSequencingTotalChart(rows) {
options: { options: {
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { 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 cleanRows = Object.values(aggregated);
const weeks = [...new Set(cleanRows.map(r => r.period))]; const weeks = [...new Set(cleanRows.map(r => r.period))];
@@ -236,3 +258,309 @@ function renderSequencingChart(rows) {
} }
}); });
} }
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 = `
<span style="width:15px;height:15px;background:${dataset.backgroundColor};display:inline-block;margin-right:8px;"></span>
${dataset.label}
`;
item.addEventListener('click', () => {
const meta = covidLineageFrequencyChart.getDatasetMeta(index);
// 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 = `
<span style="width:15px;height:15px;background:${dataset.backgroundColor};display:inline-block;margin-right:8px;"></span>
${dataset.label}
`;
item.addEventListener('click', () => {
const meta = influenzaSubtypeFrequencyChart.getDatasetMeta(index);
// 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';
});
}

View File

@@ -48,7 +48,6 @@
<div class="card shadow-sm mb-3" style="height:60vh;"> <div class="card shadow-sm mb-3" style="height:60vh;">
<div class="card-body" > <div class="card-body" >
<h5 class="fw-bold">Epidemic Trend</h5> <h5 class="fw-bold">Epidemic Trend</h5>
<p class="text-muted small report-period"> <p class="text-muted small report-period">
(based on selected epiweek range) (based on selected epiweek range)
@@ -109,11 +108,11 @@
<p class="text-muted small report-period"> <p class="text-muted small report-period">
(based on selected epiweek range) (based on selected epiweek range)
</p> </p>
<canvas id="covidLineageFrequency" style=" flex:1; max-width: 90%; max-height:45vh; float: left;"></canvas> <canvas id="covidLineageFrequency" style=" flex:1; max-width: 90%; max-height:40vh; float: left;"></canvas>
<div id="legendContainer" style=" <div id="legendContainer" style="
width:10%; width:10%;
margin-left:20px; margin-left:20px;
max-height:375px; max-height:360px;
overflow-y:auto; overflow-y:auto;
padding:8px; padding:8px;
@@ -135,7 +134,7 @@
<p class="text-muted small report-period"> <p class="text-muted small report-period">
(based on selected epiweek range) (based on selected epiweek range)
</p> </p>
<canvas id="influenzaSubtypeFrequency" style=" flex:1; max-width: 90%; max-height:45vh; float: left;"></canvas> <canvas id="influenzaSubtypeFrequency" style=" flex:1; max-width: 90%; max-height:40vh; float: left;"></canvas>
<div id="legendContainerInfluenzaSubtypeFrequency" style=" <div id="legendContainerInfluenzaSubtypeFrequency" style="
width:10%; width:10%;
margin-left:20px; margin-left:20px;

View File

@@ -27,32 +27,75 @@
</div> </div>
</div> </div>
<div class="row mt-3">
<div class="col-md-5">
<div class="card"> <div class="card">
<div class="card-body" style="height:600px;"> <div class="card-body" style="height:500px;">
<canvas id="sequencingChart"></canvas> <h5 class="fw-bold">Total Sequenced Samples Over Time</h5>
<p class="text-muted small report-period">
(based on selected epiweek range)
</p>
<canvas id="sequencingTotalChart" style=" flex:1; max-height:40vh; float: left;"></canvas>
</div> </div>
</div> </div>
</div>
<div class="col-md-7">
<div class="card">
<div class="card-body" style="height:500px;">
<h5 class="fw-bold">Influenza Subtypes Relative Frequencies Over Time</h5>
<p class="text-muted small report-period">
(based on selected epiweek range)
</p>
<canvas id="influenzaSubtypeFrequency" style=" flex:1; max-width: 85%; max-height:40vh; float: left;"></canvas>
<div id="legendContainerInfluenzaSubtypeFrequency" style="
width:15%;
margin-left:20px;
max-height:375px;
overflow-y:auto;
padding:8px;
"></div>
</div>
</div>
</div>
</div>
<div class="row mt-3"> <div class="row mt-3">
<!-- Counts --> <!-- Counts -->
<div class="col-md-6"> <div class="col-md-12">
<div class="card"> <div class="card">
<div class="card-body" style="height:600px;"> <div class="card-body" style="height:500px;">
<h6 class="fw-bold">Lineage Counts Over Time</h6> <h6 class="fw-bold">SARS-CoV-2 Lineage/Sublineage Relative Frequencies Over Time</h6>
<canvas id="sequencingCountChart"></canvas> <p class="text-muted small report-period">
(based on selected epiweek range)
</p>
{{-- <canvas id="sequencingCountChart"></canvas>--}}
<canvas id="covidLineageFrequency" style=" flex:1; max-width: 90%; max-height:40vh; float: left;"></canvas>
<div id="legendContainer" style="
width:10%;
margin-left:20px;
max-height:360px;
overflow-y:auto;
padding:8px;
"></div>
</div> </div>
</div> </div>
</div> </div>
{{-- <div class="col-md-4">--}}
{{-- <div class="card">--}}
{{-- <div class="card-body" style="height:500px;">--}}
{{-- <canvas id="sequencingChart"></canvas>--}}
{{-- <canvas id="sequencingCountChart"></canvas>--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- </div>--}}
<!-- Distribution --> <!-- Distribution -->
<div class="col-md-6">
<div class="card">
<div class="card-body" style="height:600px;">
<h6 class="fw-bold">Total Sequenced Samples Over Time</h6>
<canvas id="sequencingTotalChart" style="height: 550px;"></canvas>
</div>
</div>
</div>
</div> </div>

View File

@@ -46,6 +46,9 @@
top: 0; top: 0;
z-index: 1000; z-index: 1000;
} }
.btn{
border-radius: 0 !important;
}
.btn-theme-outline { .btn-theme-outline {
background-color: #fff; background-color: #fff;

View File

@@ -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-distributed-by-age-group', [DashboardController::class, 'covidDistributedByAgeGroup']);
Route::get('/dashboard/covid-lineage-frequency', [DashboardController::class, 'covidLineageRelativeOverTime']); Route::get('/dashboard/covid-lineage-frequency', [DashboardController::class, 'covidLineageRelativeOverTime']);
Route::get('/dashboard/influenza-relative-frequency', [DashboardController::class, 'influenzaRelativeOverTime']); 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/sentinel-map', [DashboardController::class, 'sentinelMap']);
Route::get('/dashboard/reload', [DashboardController::class, 'fetchSourceData']); Route::get('/dashboard/reload', [DashboardController::class, 'fetchSourceData']);
Route::get('/dashboard/sequencing', [DashboardController::class, 'sequencing']); Route::get('/dashboard/sequencing', [DashboardController::class, 'sequencing']);