{/* Pulse Rate */}
ECG
- {data.pulseRate?.unit ?? "--"}
+ {(data.pulseRate ?? data.heartRate)?.unit ?? "--"}
- {data.pulseRate?.value ?? "--"}
+ {(data.pulseRate ?? data.heartRate)?.value ?? "--"}
- {data.pulseRate?.value && (
+ {(data.pulseRate ?? data.heartRate)?.value && (
❤️
)}
diff --git a/src/Components/VitalsMonitor/HL7VitalsRenderer.ts b/src/Components/VitalsMonitor/HL7VitalsRenderer.ts
index f9d97b21128..1a4df38aabc 100644
--- a/src/Components/VitalsMonitor/HL7VitalsRenderer.ts
+++ b/src/Components/VitalsMonitor/HL7VitalsRenderer.ts
@@ -1,3 +1,6 @@
+import { ChannelOptions } from "./types";
+import { lerp } from "./utils";
+
interface ChannelState {
buffer: number[];
cursor: Position;
@@ -19,25 +22,6 @@ interface Position {
*/
const DURATION = 7;
-export interface ChannelOptions {
- /**
- * The baseline value for this channel.
- */
- baseline: number;
- /**
- * The minimum value that can be displayed for this channel.
- */
- lowLimit: number;
- /**
- * The maximum value that can be displayed for this channel.
- */
- highLimit: number;
- /**
- * No. of data points expected to be received per second.
- */
- samplingRate: number;
-}
-
interface Options {
/**
* The size of the canvas rendering context.
@@ -112,7 +96,7 @@ class HL7VitalsRenderer {
},
spo2: {
- color: "#2427ff",
+ color: "#03a9f4",
buffer: [],
cursor: { x: 0, y: 0 },
deltaX: w / (DURATION * spo2.samplingRate),
@@ -222,43 +206,3 @@ class HL7VitalsRenderer {
}
export default HL7VitalsRenderer;
-
-/**
- * Maps a value from one range to another.
- * Or in mathematical terms, it performs a linear interpolation.
- *
- * @param x0 The lower bound of the input range.
- * @param x1 The upper bound of the input range.
- * @param y0 The lower bound of the output range.
- * @param y1 The upper bound of the output range.
- * @returns A function that maps a value from the input range to the output range.
- * @example
- * const transform = lerp(0, 100, 0, 1);
- * transform(50); // 0.5
- * transform(100); // 1
- * transform(0); // 0
- * transform(200); // 2
- * transform(-100); // -1
- */
-const lerp = (x0: number, x1: number, y0: number, y1: number) => {
- // Original formula:
- // y = y0 + (x - x0) * (y1 - y0) / (x1 - x0)
- //
- // Simplified formula:
- //
- // 1. Take the first order partial derivative out
- // m = (y1 - y0) / (x1 - x0)
- // ∴ y = y0 + (x - x0) * m
- //
- // 2. Expanding the (x - x0) term yields:
- // ∴ y = y0 + x * m - x0 * m
- //
- // 3. Simplify the terms by grouping the constants together:
- // c = y0 - x0 * m
- // ∴ y = m * x + c
-
- const m = (y1 - y0) / (x1 - x0);
- const c = y0 - x0 * m;
-
- return (x: number) => m * x + c;
-};
diff --git a/src/Components/VitalsMonitor/VentilatorDeviceClient.ts b/src/Components/VitalsMonitor/VentilatorDeviceClient.ts
new file mode 100644
index 00000000000..0418740dcc7
--- /dev/null
+++ b/src/Components/VitalsMonitor/VentilatorDeviceClient.ts
@@ -0,0 +1,88 @@
+import { EventEmitter } from "events";
+import { VitalsDataBase, VitalsValueBase, VitalsWaveformBase } from "./types";
+
+const WAVEFORM_KEY_MAP: Record
= {
+ P: "pressure-waveform",
+ F: "flow-waveform",
+ V: "volume-waveform",
+};
+
+/**
+ * Provides the API for connecting to the Vitals Monitor WebSocket and emitting
+ * events for each observation.
+ *
+ * @example
+ * const device = new VentilatorDeviceClient("wss://vitals-middleware.local/observations/192.168.1.14");
+ *
+ * device.on("FiO2", (observation) => {
+ * console.log(observation.value);
+ * });
+ */
+class VentilatorDeviceClient extends EventEmitter {
+ constructor(socketUrl: string) {
+ super();
+ this.ws = new WebSocket(socketUrl);
+ }
+
+ ws: WebSocket;
+
+ connect() {
+ this.ws.addEventListener("message", (event) => {
+ const observations = parseObservations(event.data);
+
+ observations.forEach((observation) => {
+ if (observation.observation_id === "waveform") {
+ this.emit(WAVEFORM_KEY_MAP[observation["wave-name"]], observation);
+ } else {
+ this.emit(observation.observation_id, observation);
+ }
+ });
+ });
+ }
+
+ disconnect() {
+ this.ws.close();
+ }
+
+ on(event: EventName, listener: (data: VentilatorData) => void): this {
+ return super.on(event, listener);
+ }
+
+ emit(event: EventName, data: VentilatorData): boolean {
+ return super.emit(event, data);
+ }
+
+ once(event: EventName, listener: (data: VentilatorData) => void): this {
+ return super.once(event, listener);
+ }
+
+ off(event: EventName, listener: (data: VentilatorData) => void): this {
+ return super.off(event, listener);
+ }
+}
+
+export default VentilatorDeviceClient;
+
+export interface VentilatorVitalsValueData
+ extends VitalsDataBase,
+ VitalsValueBase {
+ observation_id: "Mode" | "PEEP" | "R.Rate" | "Insp-Time" | "FiO2";
+}
+
+export interface VentilatorVitalsWaveformData extends VitalsWaveformBase {
+ "wave-name": "P" | "F" | "V";
+}
+
+export type VentilatorData =
+ | VentilatorVitalsValueData
+ | VentilatorVitalsWaveformData;
+
+type EventName =
+ | VentilatorData["observation_id"]
+ | "pressure-waveform"
+ | "flow-waveform"
+ | "volume-waveform";
+
+const parseObservations = (data: string) => {
+ return JSON.parse(data || "[]") as VentilatorData[];
+};
diff --git a/src/Components/VitalsMonitor/VentilatorPatientVitalsMonitor.tsx b/src/Components/VitalsMonitor/VentilatorPatientVitalsMonitor.tsx
new file mode 100644
index 00000000000..a964e501f42
--- /dev/null
+++ b/src/Components/VitalsMonitor/VentilatorPatientVitalsMonitor.tsx
@@ -0,0 +1,151 @@
+import { useEffect } from "react";
+import { PatientAssetBed } from "../Assets/AssetTypes";
+import { Link } from "raviger";
+import { GENDER_TYPES } from "../../Common/constants";
+import CareIcon from "../../CAREUI/icons/CareIcon";
+import useVentilatorVitalsMonitor from "./useVentilatorVitalsMonitor";
+import { VitalsValueBase } from "./types";
+import { classNames } from "../../Utils/utils";
+import WaveformLabels from "./WaveformLabels";
+
+interface Props {
+ patientAssetBed?: PatientAssetBed;
+ socketUrl: string;
+ size?: { width: number; height: number };
+}
+
+export default function VentilatorPatientVitalsMonitor({
+ patientAssetBed,
+ socketUrl,
+ size,
+}: Props) {
+ const { connect, waveformCanvas, data, isOnline } =
+ useVentilatorVitalsMonitor();
+ const { patient, bed } = patientAssetBed ?? {};
+
+ useEffect(() => {
+ connect(socketUrl);
+ }, [socketUrl]);
+
+ return (
+
+ {patientAssetBed && (
+
+
+ {patient ? (
+
+ {patient?.name}
+
+ ) : (
+
+
+ No Patient
+
+ )}
+ {patient && (
+
+ {patient.age}y;{" "}
+ {GENDER_TYPES.find((g) => g.id === patient.gender)?.icon}
+
+ )}
+
+ {bed && (
+
+
+
+ {bed.name}
+
+
+
+ {bed.location_object?.name}
+
+
+ )}
+
+ )}
+
+
+
+ No incoming data from Ventilator
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+interface NonWaveformDataProps {
+ label: string;
+ attr?: VitalsValueBase;
+ className?: string;
+}
+
+const NonWaveformData = ({ label, attr, className }: NonWaveformDataProps) => {
+ return (
+
+
+ {label}
+ {attr?.unit ?? "--"}
+
+
+ {attr?.value ?? "--"}
+
+
+ );
+};
diff --git a/src/Components/VitalsMonitor/VentilatorWaveformsRenderer.ts b/src/Components/VitalsMonitor/VentilatorWaveformsRenderer.ts
new file mode 100644
index 00000000000..321c3239d7d
--- /dev/null
+++ b/src/Components/VitalsMonitor/VentilatorWaveformsRenderer.ts
@@ -0,0 +1,212 @@
+import { ChannelOptions } from "./types";
+import { lerp } from "./utils";
+
+interface ChannelState {
+ buffer: number[];
+ cursor: Position;
+ color: string;
+ deltaX: number;
+ transform: (value: number) => number;
+ chunkSize: number;
+ options: ChannelOptions;
+ rows: number;
+}
+
+interface Position {
+ x: number;
+ y: number;
+}
+
+/**
+ * Duration of each row on the canvas in seconds.
+ */
+const DURATION = 7;
+
+interface Options {
+ /**
+ * The size of the canvas rendering context.
+ *
+ * Height should preferably be a multiple of 4.
+ */
+ size: { width: number; height: number };
+ /**
+ * The 2D render context of the canvas to draw the vitals waveforms on.
+ */
+ foregroundRenderContext: CanvasRenderingContext2D;
+ /**
+ * The 2D render context of the canvas to draw the
+ */
+ backgroundRenderContext: CanvasRenderingContext2D;
+ /**
+ * The interval at which the canvas is rendered in milliseconds.
+ */
+ animationInterval: number;
+ /**
+ * Options for Pressure channel.
+ */
+ pressure: ChannelOptions;
+ /**
+ * Options for Flow channel.
+ */
+ flow: ChannelOptions;
+ /**
+ * Options for Volume channel.
+ */
+ volume: ChannelOptions;
+}
+
+/**
+ * Provides the API for rendering vitals waveforms on a canvas.
+ *
+ * Strategy:
+ * - Render frequency is set manually and is independent of the sampling rate.
+ * - Manages rendering of all the vitals channels.
+ */
+class VentilatorVitalsRenderer {
+ constructor(options: Options) {
+ const {
+ pressure,
+ flow,
+ volume,
+ size: { height: h, width: w },
+ } = options;
+
+ this.options = options;
+ this.state = {
+ pressure: {
+ color: "#0ffc03",
+ buffer: [],
+ cursor: { x: 0, y: 0 },
+ deltaX: w / (DURATION * pressure.samplingRate),
+ transform: lerp(pressure.lowLimit, pressure.highLimit, h * 0.33, 0),
+ chunkSize: pressure.samplingRate * options.animationInterval * 1e-3,
+ options: pressure,
+ rows: 1,
+ },
+
+ flow: {
+ color: "#ffff24",
+ buffer: [],
+ cursor: { x: 0, y: 0 },
+ deltaX: w / (DURATION * flow.samplingRate),
+ transform: lerp(flow.lowLimit, flow.highLimit, h * 0.67, h * 0.33),
+ chunkSize: flow.samplingRate * options.animationInterval * 1e-3,
+ options: flow,
+ rows: 1,
+ },
+
+ volume: {
+ color: "#03a9f4",
+ buffer: [],
+ cursor: { x: 0, y: 0 },
+ deltaX: w / (DURATION * volume.samplingRate),
+ transform: lerp(volume.lowLimit, volume.highLimit, h, h * 0.67),
+ chunkSize: volume.samplingRate * options.animationInterval * 1e-3,
+ options: volume,
+ rows: 1,
+ },
+ };
+
+ // Draw baseline for each channel.
+ this.initialize(this.state.pressure);
+ this.initialize(this.state.flow);
+ this.initialize(this.state.volume);
+
+ // Start rendering.
+ setInterval(() => {
+ this.render(this.state.pressure);
+ this.render(this.state.flow);
+ this.render(this.state.volume);
+ }, options.animationInterval);
+ }
+
+ private options: Options;
+ private state: {
+ pressure: ChannelState;
+ flow: ChannelState;
+ volume: ChannelState;
+ };
+
+ /**
+ * Appends data to the buffer of the specified channel.
+ */
+ append(channel: "pressure" | "flow" | "volume", data: number[]) {
+ const state = this.state[channel];
+ state.buffer.push(...data.map(state.transform));
+ }
+
+ private initialize(channel: ChannelState) {
+ const { foregroundRenderContext: ctx, size } = this.options;
+ const { transform, rows, options, color } = channel;
+
+ for (let row = 0; row < rows; row++) {
+ const y = transform(options.baseline) + (row * size.height) / 4;
+
+ ctx.beginPath();
+ ctx.moveTo(0, y);
+ ctx.lineTo(size.width, y);
+ ctx.strokeStyle = color;
+ ctx.stroke();
+ }
+
+ channel.cursor = { x: 0, y: transform(options.baseline) };
+ }
+
+ private render(channel: ChannelState) {
+ const { foregroundRenderContext: ctx, size } = this.options;
+ const { cursor, deltaX, transform } = channel;
+
+ const xMax = size.width * channel.rows;
+
+ ctx.beginPath();
+ ctx.moveTo(...this.correctForRow(cursor));
+
+ ctx.strokeStyle = channel.color;
+ ctx.lineWidth = 1.5;
+
+ channel.buffer.splice(0, channel.chunkSize).forEach((nextY) => {
+ cursor.x += deltaX;
+ cursor.y = nextY;
+
+ if (cursor.x > xMax) {
+ cursor.x = 0;
+ }
+
+ const [x, y] = this.correctForRow(cursor);
+
+ if (x < deltaX) {
+ ctx.beginPath();
+ ctx.moveTo(x, y);
+ } else {
+ ctx.lineTo(x, y);
+ }
+ });
+
+ ctx.stroke();
+
+ const deltaRows = Math.floor(cursor.x / size.width);
+ ctx.clearRect(
+ cursor.x - deltaRows * size.width,
+ 1 + transform(channel.options.highLimit) + (deltaRows * size.height) / 3,
+ 10,
+ size.height / 3 + 5
+ );
+ }
+
+ /**
+ * Corrects the cursor position for the appropriate row if the cursor is
+ * outside the bounds of the canvas.
+ */
+ private correctForRow(cursor: Position): [number, number] {
+ const { size } = this.options;
+
+ const deltaRows = Math.floor(cursor.x / size.width);
+
+ const x = cursor.x % size.width;
+ const y = cursor.y + (deltaRows * size.height) / 4;
+
+ return [x, y];
+ }
+}
+
+export default VentilatorVitalsRenderer;
diff --git a/src/Components/VitalsMonitor/WaveformLabels.tsx b/src/Components/VitalsMonitor/WaveformLabels.tsx
new file mode 100644
index 00000000000..a83ae2d14f7
--- /dev/null
+++ b/src/Components/VitalsMonitor/WaveformLabels.tsx
@@ -0,0 +1,17 @@
+import { classNames } from "../../Utils/utils";
+
+interface Props {
+ labels: Record;
+}
+
+export default function WaveformLabels({ labels }: Props) {
+ return (
+
+ {Object.entries(labels).map(([label, className]) => (
+
+ {label}
+
+ ))}
+
+ );
+}
diff --git a/src/Components/VitalsMonitor/types.ts b/src/Components/VitalsMonitor/types.ts
index c413181493a..046c3db348b 100644
--- a/src/Components/VitalsMonitor/types.ts
+++ b/src/Components/VitalsMonitor/types.ts
@@ -22,3 +22,22 @@ export interface VitalsWaveformBase extends VitalsDataBase {
"data-high-limit": number;
data: string;
}
+
+export interface ChannelOptions {
+ /**
+ * The baseline value for this channel.
+ */
+ baseline: number;
+ /**
+ * The minimum value that can be displayed for this channel.
+ */
+ lowLimit: number;
+ /**
+ * The maximum value that can be displayed for this channel.
+ */
+ highLimit: number;
+ /**
+ * No. of data points expected to be received per second.
+ */
+ samplingRate: number;
+}
diff --git a/src/Components/VitalsMonitor/useHL7VitalsMonitor.ts b/src/Components/VitalsMonitor/useHL7VitalsMonitor.ts
index 008ba13105f..8b14dff12e3 100644
--- a/src/Components/VitalsMonitor/useHL7VitalsMonitor.ts
+++ b/src/Components/VitalsMonitor/useHL7VitalsMonitor.ts
@@ -3,9 +3,10 @@ import HL7DeviceClient, {
HL7MonitorData,
HL7VitalsWaveformData,
} from "./HL7DeviceClient";
-import HL7VitalsRenderer, { ChannelOptions } from "./HL7VitalsRenderer";
+import HL7VitalsRenderer from "./HL7VitalsRenderer";
import useCanvas from "../../Common/hooks/useCanvas";
-import { VitalsValueBase as VitalsValue } from "./types";
+import { ChannelOptions, VitalsValueBase as VitalsValue } from "./types";
+import { getChannel } from "./utils";
export const MONITOR_RATIO = {
w: 13,
@@ -32,7 +33,9 @@ export default function useHL7VitalsMonitor() {
const waveformBackgroundCanvas = useCanvas();
// Non waveform data states.
+ const [isOnline, setIsOnline] = useState(false);
const [pulseRate, setPulseRate] = useState();
+ const [heartRate, setHeartRate] = useState();
const [bp, setBp] = useState();
const [spo2, setSpo2] = useState();
const [respiratoryRate, setRespiratoryRate] = useState();
@@ -49,6 +52,7 @@ export default function useHL7VitalsMonitor() {
const connect = useCallback(
(socketUrl: string) => {
+ setIsOnline(false);
device.current?.disconnect();
device.current = new HL7DeviceClient(socketUrl);
@@ -62,6 +66,8 @@ export default function useHL7VitalsMonitor() {
)
return;
+ setIsOnline(true);
+
renderer.current = new HL7VitalsRenderer({
foregroundRenderContext: waveformForegroundCanvas.contextRef.current!,
backgroundRenderContext: waveformBackgroundCanvas.contextRef.current!,
@@ -79,7 +85,8 @@ export default function useHL7VitalsMonitor() {
const hook = (set: (data: any) => void) => (d: HL7MonitorData) =>
set(d);
- device.current!.on("heart-rate", hook(setPulseRate));
+ device.current!.on("pulse-rate", hook(setPulseRate));
+ device.current!.on("heart-rate", hook(setHeartRate));
device.current!.on("SpO2", hook(setSpo2));
device.current!.on("respiratory-rate", hook(setRespiratoryRate));
device.current!.on("body-temperature1", hook(setTemperature1));
@@ -120,6 +127,7 @@ export default function useHL7VitalsMonitor() {
},
data: {
pulseRate,
+ heartRate,
bp,
spo2,
respiratoryRate,
@@ -127,20 +135,10 @@ export default function useHL7VitalsMonitor() {
temperature2,
},
device,
+ isOnline,
};
}
-const getChannel = (observation: HL7VitalsWaveformData): ChannelOptions => {
- return {
- samplingRate: parseInt(
- observation["sampling rate"]?.replace("/sec", "") ?? "-1"
- ),
- baseline: observation["data-baseline"] ?? 0,
- lowLimit: observation["data-low-limit"] ?? 0,
- highLimit: observation["data-high-limit"] ?? 0,
- };
-};
-
const ingestTo = (
vitalsRenderer: HL7VitalsRenderer,
channel: "ecg" | "pleth" | "spo2"
diff --git a/src/Components/VitalsMonitor/useVentilatorVitalsMonitor.ts b/src/Components/VitalsMonitor/useVentilatorVitalsMonitor.ts
new file mode 100644
index 00000000000..ae07a9d0813
--- /dev/null
+++ b/src/Components/VitalsMonitor/useVentilatorVitalsMonitor.ts
@@ -0,0 +1,142 @@
+import { useCallback, useRef, useState } from "react";
+import useCanvas from "../../Common/hooks/useCanvas";
+import { ChannelOptions, VitalsValueBase as VitalsValue } from "./types";
+import VentilatorDeviceClient, {
+ VentilatorData,
+ VentilatorVitalsWaveformData,
+} from "./VentilatorDeviceClient";
+import VentilatorVitalsRenderer from "./VentilatorWaveformsRenderer";
+import { getChannel } from "./utils";
+
+export const MONITOR_RATIO = {
+ w: 13,
+ h: 11,
+};
+const MONITOR_SCALE = 38;
+const MONITOR_WAVEFORMS_CANVAS_SIZE = {
+ width: MONITOR_RATIO.h * MONITOR_SCALE,
+ height: MONITOR_RATIO.h * MONITOR_SCALE,
+};
+// const MONITOR_SIZE = {
+// width: MONITOR_RATIO.w * MONITOR_SCALE,
+// height: MONITOR_RATIO.h * MONITOR_SCALE,
+// };
+
+export default function useVentilatorVitalsMonitor() {
+ const waveformForegroundCanvas = useCanvas();
+ const waveformBackgroundCanvas = useCanvas();
+
+ // Non waveform data states.
+ const [isOnline, setIsOnline] = useState(false);
+ const [peep, setPeep] = useState();
+ const [respRate, setRespRate] = useState();
+ const [inspTime, setInspTime] = useState();
+ const [fio2, setFio2] = useState();
+
+ // Waveform data states.
+ const device = useRef();
+ const renderer = useRef(null);
+
+ const pressureOptionsRef = useRef();
+ const flowOptionsRef = useRef();
+ const volumeOptionsRef = useRef();
+
+ const connect = useCallback(
+ (socketUrl: string) => {
+ setIsOnline(false);
+ device.current?.disconnect();
+
+ device.current = new VentilatorDeviceClient(socketUrl);
+ device.current.connect();
+
+ function obtainRenderer() {
+ if (
+ !pressureOptionsRef.current ||
+ !flowOptionsRef.current ||
+ !volumeOptionsRef.current
+ )
+ return;
+
+ setIsOnline(true);
+
+ renderer.current = new VentilatorVitalsRenderer({
+ foregroundRenderContext: waveformForegroundCanvas.contextRef.current!,
+ backgroundRenderContext: waveformBackgroundCanvas.contextRef.current!,
+ size: MONITOR_WAVEFORMS_CANVAS_SIZE,
+ animationInterval: 50,
+ pressure: pressureOptionsRef.current,
+ flow: flowOptionsRef.current,
+ volume: volumeOptionsRef.current,
+ });
+
+ const _renderer = renderer.current;
+ device.current!.on(
+ "pressure-waveform",
+ ingestTo(_renderer, "pressure")
+ );
+ device.current!.on("flow-waveform", ingestTo(_renderer, "flow"));
+ device.current!.on("volume-waveform", ingestTo(_renderer, "volume"));
+
+ const hook = (set: (data: any) => void) => (d: VentilatorData) =>
+ set(d);
+ device.current!.on("PEEP", hook(setPeep));
+ device.current!.on("R.Rate", hook(setRespRate));
+ device.current!.on("Insp-Time", hook(setInspTime));
+ device.current!.on("FiO2", hook(setFio2));
+ }
+
+ device.current.once("pressure-waveform", (observation) => {
+ pressureOptionsRef.current = getChannel(
+ observation as VentilatorVitalsWaveformData
+ );
+ obtainRenderer();
+ });
+
+ device.current.once("flow-waveform", (observation) => {
+ flowOptionsRef.current = getChannel(
+ observation as VentilatorVitalsWaveformData
+ );
+ obtainRenderer();
+ });
+
+ device.current.once("volume-waveform", (observation) => {
+ volumeOptionsRef.current = getChannel(
+ observation as VentilatorVitalsWaveformData
+ );
+ obtainRenderer();
+ });
+ },
+ [waveformForegroundCanvas.contextRef, waveformBackgroundCanvas]
+ );
+
+ return {
+ connect,
+ waveformCanvas: {
+ foreground: waveformForegroundCanvas,
+ background: waveformBackgroundCanvas,
+ size: MONITOR_WAVEFORMS_CANVAS_SIZE,
+ },
+ data: {
+ peep,
+ respRate,
+ inspTime,
+ fio2,
+ },
+ device,
+ isOnline,
+ };
+}
+
+const ingestTo = (
+ vitalsRenderer: VentilatorVitalsRenderer,
+ channel: "pressure" | "flow" | "volume"
+) => {
+ return (observation: VentilatorData) => {
+ vitalsRenderer.append(
+ channel,
+ (observation as VentilatorVitalsWaveformData).data
+ .split(" ")
+ .map((x) => parseInt(x)) || []
+ );
+ };
+};
diff --git a/src/Components/VitalsMonitor/utils.ts b/src/Components/VitalsMonitor/utils.ts
new file mode 100644
index 00000000000..bd4b05fad58
--- /dev/null
+++ b/src/Components/VitalsMonitor/utils.ts
@@ -0,0 +1,52 @@
+import { ChannelOptions, VitalsWaveformBase } from "./types";
+
+/**
+ * Maps a value from one range to another.
+ * Or in mathematical terms, it performs a linear interpolation.
+ *
+ * @param x0 The lower bound of the input range.
+ * @param x1 The upper bound of the input range.
+ * @param y0 The lower bound of the output range.
+ * @param y1 The upper bound of the output range.
+ * @returns A function that maps a value from the input range to the output range.
+ * @example
+ * const transform = lerp(0, 100, 0, 1);
+ * transform(50); // 0.5
+ * transform(100); // 1
+ * transform(0); // 0
+ * transform(200); // 2
+ * transform(-100); // -1
+ */
+export const lerp = (x0: number, x1: number, y0: number, y1: number) => {
+ // Original formula:
+ // y = y0 + (x - x0) * (y1 - y0) / (x1 - x0)
+ //
+ // Simplified formula:
+ //
+ // 1. Take the first order partial derivative out
+ // m = (y1 - y0) / (x1 - x0)
+ // ∴ y = y0 + (x - x0) * m
+ //
+ // 2. Expanding the (x - x0) term yields:
+ // ∴ y = y0 + x * m - x0 * m
+ //
+ // 3. Simplify the terms by grouping the constants together:
+ // c = y0 - x0 * m
+ // ∴ y = m * x + c
+
+ const m = (y1 - y0) / (x1 - x0);
+ const c = y0 - x0 * m;
+
+ return (x: number) => m * x + c;
+};
+
+export const getChannel = (observation: VitalsWaveformBase): ChannelOptions => {
+ return {
+ samplingRate: parseInt(
+ observation["sampling rate"]?.replace("/sec", "") ?? "-1"
+ ),
+ baseline: observation["data-baseline"] ?? 0,
+ lowLimit: observation["data-low-limit"] ?? 0,
+ highLimit: observation["data-high-limit"] ?? 0,
+ };
+};