No SOFA trend chart or organ-system timeline No GCS trend chart or component history No qSOFA history view
122 lines
3.6 KiB
Vue
122 lines
3.6 KiB
Vue
<script setup>
|
||
import { computed, shallowRef, markRaw } from 'vue'
|
||
import { Chart } from 'vue-chartjs'
|
||
import { Chart as ChartJS, registerables } from 'chart.js'
|
||
import { formatTime } from '@/composables/chartFormat'
|
||
|
||
ChartJS.register(...registerables)
|
||
|
||
const props = defineProps({ history: { type: Array, required: true } })
|
||
|
||
const ORGAN_DATASETS = [
|
||
{ label: 'Respiratory', key: 'respiratoryScore', color: '#3b82f6' },
|
||
{ label: 'Coagulation', key: 'coagulationScore', color: '#8b5cf6' },
|
||
{ label: 'Liver', key: 'liverScore', color: '#f59e0b' },
|
||
{ label: 'Cardiovascular', key: 'cardiovascularScore', color: '#ef4444' },
|
||
{ label: 'CNS', key: 'cnsScore', color: '#ec4899' },
|
||
{ label: 'Renal', key: 'renalScore', color: '#06b6d4' },
|
||
]
|
||
|
||
function sofaRiskBorder(score) {
|
||
if (score >= 10) return '#dc2626'
|
||
if (score >= 6) return '#f59e0b'
|
||
return '#22c55e'
|
||
}
|
||
|
||
const sortedHistory = computed(() =>
|
||
[...props.history].sort((a, b) => new Date(a.calculatedAt) - new Date(b.calculatedAt)),
|
||
)
|
||
|
||
const chartData = computed(() => {
|
||
const sorted = sortedHistory.value
|
||
const labels = sorted.map(h => formatTime(h.calculatedAt))
|
||
|
||
const organDatasets = ORGAN_DATASETS.map(({ label, key, color }) => ({
|
||
type: 'line',
|
||
label,
|
||
data: sorted.map(h => h[key] ?? 0),
|
||
backgroundColor: `${color}66`,
|
||
borderColor: color,
|
||
borderWidth: 1,
|
||
fill: true,
|
||
stack: 'organs',
|
||
pointRadius: 0,
|
||
tension: 0.2,
|
||
order: 2,
|
||
}))
|
||
|
||
return {
|
||
labels,
|
||
datasets: [
|
||
...organDatasets,
|
||
{
|
||
type: 'line',
|
||
label: 'SOFA Total',
|
||
data: sorted.map(h => h.totalScore),
|
||
borderColor: '#111827',
|
||
backgroundColor: 'transparent',
|
||
pointBackgroundColor: sorted.map(h => sofaRiskBorder(h.totalScore)),
|
||
pointBorderColor: sorted.map(h => sofaRiskBorder(h.totalScore)),
|
||
pointRadius: 4,
|
||
borderWidth: 2,
|
||
fill: false,
|
||
tension: 0.3,
|
||
order: 1,
|
||
},
|
||
],
|
||
}
|
||
})
|
||
|
||
const chartOptions = shallowRef(markRaw({
|
||
responsive: true,
|
||
maintainAspectRatio: true,
|
||
animation: {
|
||
duration: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 400,
|
||
},
|
||
interaction: { mode: 'index', intersect: false },
|
||
scales: {
|
||
x: { stacked: true },
|
||
y: {
|
||
stacked: true,
|
||
min: 0,
|
||
max: 24,
|
||
title: { display: true, text: 'SOFA Score' },
|
||
},
|
||
},
|
||
plugins: {
|
||
legend: {
|
||
display: true,
|
||
position: 'bottom',
|
||
labels: { boxWidth: 12, font: { size: 11 } },
|
||
},
|
||
tooltip: {
|
||
callbacks: {
|
||
footer(items) {
|
||
const total = items.find(i => i.dataset.label === 'SOFA Total')
|
||
if (!total) return ''
|
||
const score = total.parsed.y
|
||
if (score >= 10) return 'Risk: High (≥10)'
|
||
if (score >= 6) return 'Risk: Moderate (6–9)'
|
||
return 'Risk: Low (0–5)'
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}))
|
||
</script>
|
||
|
||
<template>
|
||
<div class="w-full min-w-0 rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
|
||
<h3 class="mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">SOFA Score Over Time</h3>
|
||
<p class="mb-4 text-xs text-gray-500 dark:text-gray-400">
|
||
Stacked areas show per-organ contributions; line shows total score.
|
||
<span class="text-green-600 dark:text-green-400">0–5</span>,
|
||
<span class="text-amber-600 dark:text-amber-400">6–9</span>,
|
||
<span class="text-red-600 dark:text-red-400">10+</span>.
|
||
</p>
|
||
<div class="aspect-video w-full min-h-0 overflow-hidden">
|
||
<Chart type="line" :data="chartData" :options="chartOptions" />
|
||
</div>
|
||
</div>
|
||
</template>
|