Merge branch 'master' of github.com:khantey1998/nrml-dashboard
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,27 +2,80 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\Surveillance;
|
||||
use App\Models\SurveillanceCase;
|
||||
use App\Models\CaseLabResult;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DataRetrievalService
|
||||
{
|
||||
|
||||
protected $apiBaseUrl;
|
||||
protected $apiUsername;
|
||||
protected $apiPassword;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiBaseUrl = config('services.nphl_api.url');
|
||||
$this->apiUsername = config('services.nphl_api.username');
|
||||
$this->apiPassword = config('services.nphl_api.password');
|
||||
}
|
||||
|
||||
public function get($endpoint)
|
||||
{
|
||||
try {
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $this->apiBaseUrl . $endpoint);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_ENCODING, '');
|
||||
curl_setopt($ch, CURLOPT_MAXREDIRS, 10);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
|
||||
curl_setopt($ch, CURL_HTTP_VERSION_1_1, 0);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
|
||||
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $this->apiUsername . ":" . $this->apiPassword);
|
||||
$resp = curl_exec($ch);
|
||||
$b="";
|
||||
if($e = curl_error($ch)){
|
||||
$b= $e;
|
||||
}else{
|
||||
$b= $resp;
|
||||
}
|
||||
curl_close($ch);
|
||||
return $b;
|
||||
|
||||
} catch (RequestException $e) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => $e->getMessage(),
|
||||
'status' => optional($e->response)->status()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getSurveillanceData()
|
||||
{
|
||||
try{
|
||||
$toDate = now();
|
||||
$this->getSARICases(now()->subDays(config('app.lookback_days.SARI'))->toDateString(), $toDate); // done
|
||||
$this->getILICases(now()->subDays(config('app.lookback_days.ILI'))->toDateString(), $toDate); // done
|
||||
$this->getLBMCases(now()->subDays(config('app.lookback_days.LBM'))->toDateString(), $toDate); // done
|
||||
$this->getAFICases(now()->subDays(config('app.lookback_days.AFI'))->toDateString(), $toDate); // done
|
||||
$this->getNSDCases(now()->subDays(config('app.lookback_days.NDS'))->toDateString(), $toDate);
|
||||
$this->getSEQCases(now()->subDays(config('app.lookback_days.SEQ'))->toDateString(), $toDate); // done
|
||||
Log::channel('jobs')->info($toDate->toDateString(). ' Service Reload Data Successfully Ran');
|
||||
$lookbackDays = config('app.lookback_days');
|
||||
$surveillances = DB::connection('mysql')->select("select * from surveillances");
|
||||
foreach ($surveillances as $surveillance){
|
||||
$surveillance_data = $this->get('api/labsurveil.php?surveillance_id='.$surveillance->id.'&start_date='.now()->subDays($lookbackDays[$surveillance->code])->toDateString());
|
||||
$data = json_decode(preg_replace('/^\xEF\xBB\xBF/', '', $surveillance_data));
|
||||
@$this->insert_surveillance_cases((array)$data->laboratory_cases);
|
||||
@$this->insert_surveillance_case_lab_results((array)$data->laboratory_results);
|
||||
|
||||
}
|
||||
|
||||
//$this->getSARICases(now()->subDays(config('app.lookback_days.SARI'))->toDateString(), $toDate); // done
|
||||
// $this->getILICases(now()->subDays(config('app.lookback_days.ILI'))->toDateString(), $toDate); // done
|
||||
// $this->getLBMCases(now()->subDays(config('app.lookback_days.LBM'))->toDateString(), $toDate); // done
|
||||
// $this->getAFICases(now()->subDays(config('app.lookback_days.AFI'))->toDateString(), $toDate); // done
|
||||
// $this->getNSDCases(now()->subDays(config('app.lookback_days.NDS'))->toDateString(), $toDate);
|
||||
// $this->getSEQCases(now()->subDays(config('app.lookback_days.SEQ'))->toDateString(), $toDate); // done
|
||||
Log::channel('jobs')->info(now()->toDateString(). ' Service Reload Data Successfully Ran');
|
||||
return true;
|
||||
}
|
||||
catch (\Exception $e){
|
||||
@@ -35,6 +88,7 @@ class DataRetrievalService
|
||||
|
||||
public function getSARICases($dateFrom, $dateTo=NULL)
|
||||
{
|
||||
|
||||
$cond = "";
|
||||
$cond .= " and patient.patdate >= '".date('Y-m-d', strtotime($dateFrom))."' ";
|
||||
$cond .= (!empty($dateTo)) ? " and patient.patdate <='".date('Y-m-d', strtotime($dateTo))."' ":"";
|
||||
|
||||
@@ -35,4 +35,10 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'nphl_api' => [
|
||||
'url' => env('NPHL_API_URL'),
|
||||
'username' => env('NPHL_API_USERNAME'),
|
||||
'password' => env('NPHL_API_PASSWORD'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -75,8 +75,8 @@ class DashboardFilter {
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
for (let y = year-20; y <= year; y++) {
|
||||
|
||||
for (let y = year; y >= year-20; y--) {
|
||||
this.startYear.innerHTML += `<option value="${y}">${y}</option>`;
|
||||
this.endYear.innerHTML += `<option value="${y}">${y}</option>`;
|
||||
|
||||
|
||||
@@ -931,13 +931,13 @@ function addPositivityLegend() {
|
||||
<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;">Influenza Subtypes</div>
|
||||
<div><span style="background: #2563eb; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/H1N1pdm</div>
|
||||
<div><span style="background: #ff0200; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/H1N1pdm</div>
|
||||
<div><span style="background: #11de9d; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/H3N2</div>
|
||||
<div><span style="background: #b91081; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/H9N2</div>
|
||||
<div><span style="background: #ad850d; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/H5N1</div>
|
||||
<div><span style="background: #047393; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/Unsubtypable</div>
|
||||
<div><span style="background: #9333ea; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>B/Yam</div>
|
||||
<div><span style="background: #1021b9; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>B/Vic</div>
|
||||
<div><span style="background: #086037; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>B/Vic</div>
|
||||
<div><span style="background: #890512; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>B/Unsubtypable</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1036,10 +1036,10 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
|
||||
|
||||
function getColorByPathogen(name) {
|
||||
const colors = {
|
||||
"A/H1N1pdm": "#2563eb",
|
||||
"A/H1N1pdm": "#ff0200",
|
||||
"A/H3N2": "#11de9d",
|
||||
"B/Yam": "#9333ea",
|
||||
"B/Vic" : "#1021b9",
|
||||
"B/Vic" : "#086037",
|
||||
"A/H9N2": "#b91081",
|
||||
"A/H5N1": "#ad850d",
|
||||
"A/Unsubtypable": "#047393",
|
||||
|
||||
@@ -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))];
|
||||
@@ -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';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
|
||||
<div class="card shadow-sm mb-3" style="height:60vh;">
|
||||
<div class="card-body" >
|
||||
|
||||
<h5 class="fw-bold">Epidemic Trend</h5>
|
||||
<p class="text-muted small report-period">
|
||||
(based on selected epiweek range)
|
||||
@@ -108,11 +107,11 @@
|
||||
<p class="text-muted small report-period">
|
||||
(based on selected epiweek range)
|
||||
</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="
|
||||
width:10%;
|
||||
margin-left:20px;
|
||||
max-height:375px;
|
||||
max-height:360px;
|
||||
overflow-y:auto;
|
||||
padding:8px;
|
||||
|
||||
@@ -134,7 +133,7 @@
|
||||
<p class="text-muted small report-period">
|
||||
(based on selected epiweek range)
|
||||
</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="
|
||||
width:10%;
|
||||
margin-left:20px;
|
||||
|
||||
@@ -27,32 +27,75 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" style="height:600px;">
|
||||
<canvas id="sequencingChart"></canvas>
|
||||
<div class="row mt-3">
|
||||
|
||||
<div class="col-md-5">
|
||||
<div class="card">
|
||||
<div class="card-body" style="height:500px;">
|
||||
<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 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">
|
||||
|
||||
<!-- Counts -->
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body" style="height:600px;">
|
||||
<h6 class="fw-bold">Lineage Counts Over Time</h6>
|
||||
<canvas id="sequencingCountChart"></canvas>
|
||||
<div class="card-body" style="height:500px;">
|
||||
<h6 class="fw-bold">SARS-CoV-2 Lineage/Sublineage Relative Frequencies Over Time</h6>
|
||||
<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 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 -->
|
||||
<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>
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
.btn{
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.btn-theme-outline {
|
||||
background-color: #fff;
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user