<template>
<div>
<div class="wrap">
<h1>Patient vitals, one reference range</h1>
<p class="lead">
The normal band is declared once and drawn in all twelve panels, so the
question "who is outside the range" is answered by looking, and P-07's
tachycardia is unmissable. Hover anywhere: one card reads every patient
at the same hour.
</p>
<div class="chart-wrap">
<div id="chart">
<apexchart
type="line"
:options="chartOptions"
:series="series"
></apexchart>
</div>
</div>
<div class="note">
A trellis-scoped annotation is declared once on the host and projected
into every panel through each panel's own scale, so twelve
hand-annotated charts collapse into one
<code>annotations.yaxis</code> entry. An annotation with
<code>scope: 'P-07'</code> draws only in that patient's panel (the
medication event). <code>trellis.tooltip: 'grid'</code>
composes ONE tooltip card with one row per panel at the hovered hour,
honoring the y formatter, and works even on panels a virtualized grid
has not mounted. Trellis is a premium feature; without a license it
renders with a trial watermark.
</div>
</div>
</div>
</template>
<script>
import VueApexCharts from 'vue-apexcharts'
import 'apexcharts/features/trellis'
// Twelve patients, one reference range: the 60-100 bpm normal band is
// declared ONCE on the host and drawn in every panel, projected through the
// shared scale, so no panel's band can ever be wrong. A second annotation
// carries `scope: 'P-07'`: the medication event line appears only in that
// patient's panel. Hover anywhere: `tooltip: 'grid'` shows ONE card with
// every patient's reading at the hovered hour.
//
// Deterministic vitals so the e2e snapshot is stable.
function mulberry32(seed) {
return function () {
seed |= 0
seed = (seed + 0x6d2b79f5) | 0
var t = Math.imul(seed ^ (seed >>> 15), 1 | seed)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
var T0 = new Date('01 Jan 2025').getTime()
function heartRate(seed, base, tachyFrom) {
var rand = mulberry32(seed)
var out = []
var ts = T0
var val = base
for (var i = 0; i < 48; i++) {
val += (rand() - 0.5) * 6
var v = val
// P-07 trends tachycardic until the 30h medication event pulls it back.
if (tachyFrom && i >= tachyFrom && i < 30) v = val + 28
if (tachyFrom && i >= 30) v = val + Math.max(0, 28 - (i - 30) * 3)
out.push([ts, Math.round(v)])
ts += 3600000 // hourly
}
return out
}
var patientSeries = []
for (var p = 1; p <= 12; p++) {
var id = 'P-' + (p < 10 ? '0' + p : p)
patientSeries.push({
name: 'Heart rate',
patient: id,
data: heartRate(p * 7, 68 + (p % 5) * 4, p === 7 ? 12 : 0),
})
}
export default {
components: {
apexchart: VueApexCharts,
},
data: function () {
return {
series: patientSeries,
chartOptions: {
chart: {
id: 'vitalsTrellis',
type: 'line',
animations: {
enabled: false,
},
},
trellis: {
by: 'patient',
minPanelWidth: 240,
panelHeight: 130,
gap: 12,
tooltip: 'grid',
},
colors: ['#0F766E'],
stroke: {
width: 2,
curve: 'straight',
},
annotations: {
yaxis: [
{
y: 60,
y2: 100,
fillColor: '#22C55E',
opacity: 0.1,
},
],
xaxis: [
{
x: new Date('01 Jan 2025').getTime() + 30 * 3600000,
scope: 'P-07',
borderColor: '#D97706',
strokeDashArray: 4,
label: {
text: 'medication',
orientation: 'horizontal',
style: {
color: '#92400E',
background: '#FEF3C7',
fontSize: '10px',
},
},
},
],
},
xaxis: {
type: 'datetime',
},
yaxis: {
labels: {
formatter: function (val) {
return Math.round(val) + ' bpm'
},
},
},
dataLabels: {
enabled: false,
},
tooltip: {
x: {
format: 'dd MMM HH:mm',
},
},
},
}
},
}
</script>
<style>
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #fafbfc;
color: #2c3e50;
margin: 0;
padding: 32px 16px;
}
.wrap {
max-width: 1040px;
margin: 0 auto;
}
h1 {
font-size: 22px;
margin: 0 0 8px;
}
.lead {
color: #5b6b78;
line-height: 1.5;
margin: 0 0 20px;
}
.chart-wrap {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
}
.note {
background: #f0fdf4;
border-left: 3px solid #16a34a;
padding: 12px 16px;
font-size: 13px;
color: #333;
border-radius: 2px;
line-height: 1.6;
margin: 26px auto 0;
}
.note code {
background: #dcfce7;
padding: 1px 5px;
border-radius: 3px;
}
</style>