ecg-data-processor
Version:
A JavaScript library for processing and analyzing ECG data, including noise filtering, PQRST wave detection, and clinical indicator calculations.
973 lines (906 loc) • 38.9 kB
JavaScript
/**
*
* ECG Data Process
*
* This script is used to process the ECG data.
* Input: ECG data in the form of an array of time and voltage values.
* Output: Processed ECG data in the form as below:
* {
* summary: {
* hr: 0,
* st: 0,
* pr: 0,
* qrs: 0,
* }
* segments:[
* {
* start_time: 0,
* end_time: 0,
* summary: {
* hr: 0,
* st: 0,
* pr: 0,
* qrs: 0,
* }
* beats: [
* {
* valid: true,
* start_time: 0,
* end_time: 0,
* p:{
* start_time: 0,
* end_time: 0,
* peak_time: 0,
* peak_voltage: 0,
* },
* q:{
* start_time: 0,
* end_time: 0,
* peak_time: 0,
* peak_voltage: 0,
* },
* r:{
* start_time: 0,
* end_time: 0,
* peak_time: 0,
* peak_voltage: 0,
* },
* s:{
* start_time: 0,
* end_time: 0,
* peak_time: 0,
* peak_voltage: 0,
* },
* t:{
* start_time: 0,
* end_time: 0,
* peak_time: 0,
* peak_voltage: 0,
* }
* }
* ]
* }
* ],
* }
*
*
* Usage:
* let resultObj=ecg_data_process(ecgData, frequency);
* console.log(resultObj.getResult());
* console.log(resultObj.getOriginalData());
* console.log(resultObj.getOriginalFrequency());
*/
function ecg_data_process_v1(ecgData, frequency, options) {
function resultConstructor(ecgData, frequency, options) {
this._option = options;
if (options.useDirectData) {
this._data = ecgData;
} else {
this._data = ecgData.map((val, idx) => [idx * 1000 / frequency, val]);
}
this._frequency = frequency;
this._result = {
summary: {
hr: 0,
st: 0,
pr: 0,
qrs: 0
},
segments: []
};
this._originalData = ecgData;
this._originalFrequency = frequency;
this.filterResult = function (result) {
let resultCopy = JSON.parse(JSON.stringify(result));
if (this._option.debug) {
return resultCopy;
}
for (const segment of resultCopy.segments) {
delete segment._data;
for (const beat of segment.beats) {
delete beat.p._data;
delete beat.q._data;
delete beat.r._data;
delete beat.s._data;
delete beat.t._data;
delete beat._data;
delete beat._baseline;
delete beat._cut_start;
delete beat._cut_end;
}
}
return resultCopy;
}
this.getResult = function () {
return this.filterResult(this._result);
};
this.getOriginalData = function () {
return this._originalData;
};
this.getOriginalFrequency = function () {
return this._originalFrequency;
}
}
function splitData(ecgData) {
let data = [];
let segments = [];
//iterate ecgData
for (let i = 0; i < ecgData.length; i++) {
//if the data is not a number
if (isNaN(ecgData[i][1]) || ecgData[i][1] === null) {
//push the data to segments
if (data.length > 0) {
segments.push(data);
data = [];
}
} else {
//push the data to data
data.push(ecgData[i]);
}
}
if (data.length > 0) {
segments.push(data);
}
return segments;
}
function aggregation(data, groupSize = 5) {
let result = {
max: -65535,
min: 65535,
middle: 0,
data: []
};
//should by 500 data/sec * 30 seconds
let dataArr = data || [];
let dataGroup = [];
for (let i = 0; i < dataArr.length; i++) {
const voltage = dataArr[i];
const prevVoltage = dataArr[i - 1] || null;
let valid = true;
if (typeof prevVoltage === 'number') {
//check if valid
if (Math.abs(voltage - prevVoltage) > 2) {
valid = false;
}
}
if (valid) {
result.max = Math.max(result.max, voltage);
result.min = Math.min(result.min, voltage);
dataGroup.push(voltage);
}
if (dataGroup.length >= groupSize) {
// if middle is larger or smaller than first and last, then it is a peak
let first = dataGroup[0],
last = dataGroup[dataGroup.length - 1],
maxVal = Math.max.apply(null, dataGroup),
minVal = Math.min.apply(null, dataGroup);
if (maxVal > first && maxVal > last) {
result.data.push(maxVal);
dataGroup = [];
} else if (minVal < first && minVal < last) {
result.data.push(minVal);
dataGroup = [];
} else {
//get average voltage of dataGroup
let sum = 0;
for (let j = 0; j < dataGroup.length; j++) {
sum += dataGroup[j];
}
let avg = sum / dataGroup.length;
result.data.push(avg);
dataGroup = [];
}
}
}
if (result.max < 1.5 && result.min > -1.5) {
result.middle = 0;
} else {
result.middle = (result.max + result.min) / 2;
for (let i = 0; i < result.data.length; i++) {
let value = result.data[i];
result.data[i] = value - result.middle;
}
}
return result.data;
}
function smoothSignal(data, windowSize = 5) {
return data.map((val, idx) => {
const start = Math.max(0, idx - Math.floor(windowSize / 2));
const end = Math.min(data.length, idx + Math.ceil(windowSize / 2));
const window = data.slice(start, end);
return [val[0], window.reduce((sum, val) => sum + val[1], 0) / window.length];
});
}
function medianFilter(data, windowSize = 20) {
const result = [];
const halfWindow = Math.floor(windowSize / 2);
for (let i = 0; i < data.length; i++) {
// Define the window around the current point
const window = [];
for (let j = Math.max(0, i - halfWindow); j <= Math.min(data.length - 1, i + halfWindow); j++) {
window.push(data[j][1]); // Glucose value is at index 1
}
// Sort the window and take the median
window.sort((a, b) => a - b);
result.push([data[i][0], window[Math.floor(window.length / 2)]]);
}
return result;
}
function meanFilter(data, windowSize = 20) {
const result = [];
const halfWindow = Math.floor(windowSize / 2);
for (let i = 0; i < data.length; i++) {
let sum = 0;
let count = 0;
for (let j = Math.max(0, i - halfWindow); j <= Math.min(data.length - 1, i + halfWindow); j++) {
sum += data[j][1]; // Glucose value is at index 1
count++;
}
result.push([data[i][0], sum / count]); // Push [time, average glucose value]
}
return result;
}
function lowPassFilter(data, alpha = 0.15) {
const result = [];
let previous = data[0][1]; // Start with the first glucose value
for (let i = 0; i < data.length; i++) {
// Apply low-pass filter formula
let filtered = alpha * data[i][1] + (1 - alpha) * previous;
result.push([data[i][0], filtered]); // Push [time, filtered glucose value]
previous = filtered;
}
return result;
}
// Function to find local minima (troughs)
/*function findTroughs(ecgData, direction = -1, minDistance = 10) {
let troughs = [];
if (direction < 0) {
// Loop through the ECG data and find local minima
for (let i = ecgData.length - 2; i >= 1; i--) {
if (ecgData[i][1] < ecgData[i - 1][1] && ecgData[i][1] < ecgData[i + 1][1]) {
if (troughs.length > 0) {
if (Math.abs(i - troughs[troughs.length - 1]) > minDistance) {
troughs.push(i); // Store the index of the trough
}
} else {
troughs.push(i); // Store the index of the trough
}
}
}
} else {
// Loop through the ECG data and find local minima
for (let i = 1; i < ecgData.length - 1; i++) {
if (ecgData[i][1] < ecgData[i - 1][1] && ecgData[i][1] < ecgData[i + 1][1]) {
if (troughs.length > 0) {
if (Math.abs(i - troughs[troughs.length - 1]) > minDistance) {
troughs.push(i); // Store the index of the trough
}
} else {
troughs.push(i); // Store the index of the trough
}
}
}
}
return troughs;
}*/
function findPeaks(ecgData, direction = -1, minDistance = 10, minPeakHeight = 0.1) {
let peaks = [];
let through = null;
if (direction < 0) {
// Loop through the ECG data and find local minima
for (let i = ecgData.length - 2; i >= 1; i--) {
if (typeof through !== 'number') {
through = ecgData[i][1];
} else {
through = Math.min(through, ecgData[i][1]);
}
if (ecgData[i][1] > ecgData[i + 1][1]) {
let j = i - 1;
while (j >= 0 && ecgData[j][1] === ecgData[i][1]) {
j--;
}
if (j >= 0 && ecgData[j][1] < ecgData[i][1]) {
let peak = Math.round((i + j) / 2);
let peakValue = ecgData[peak][1];
if (peakValue - through >= minPeakHeight) {
if (peaks.length > 0) {
if (Math.abs(peak - peaks[peaks.length - 1]) > minDistance) {
peaks.push(peak); // Store the index of the peak
through = null;
}
} else {
peaks.push(peak); // Store the index of the peak
through = null;
}
}
}
}
}
} else {
// Loop through the ECG data and find local minima
for (let i = 1; i < ecgData.length - 1; i++) {
if (typeof through !== 'number') {
through = ecgData[i][1];
} else {
through = Math.min(through, ecgData[i][1]);
}
if (ecgData[i][1] > ecgData[i - 1][1]) {
let j = i + 1;
while (j < ecgData.length && ecgData[j][1] === ecgData[i][1]) {
j++;
}
if (j < ecgData.length && ecgData[j][1] < ecgData[i][1]) {
let peak = Math.round((i + j) / 2);
let peakValue = ecgData[peak][1];
if (peakValue - through >= minPeakHeight) {
if (peaks.length > 0) {
if (Math.abs(peak - peaks[peaks.length - 1]) > minDistance) {
peaks.push(peak); // Store the index of the peak
through = null;
}
} else {
peaks.push(peak); // Store the index of the peak
through = null;
}
}
}
}
}
}
return peaks;
}
function calculateSlope(signal) {
const slope = [];
for (let i = 1; i < signal.length; i++) {
slope.push(signal[i][1] - signal[i - 1][1]); // First-order difference
}
return slope;
}
function getRPeaks(seg, options) {
let rPeaks = [];
let smoothed = smoothSignal(seg, options.smoothWindowSize);
// console.log('smoothed:', smoothed);
const slope = calculateSlope(smoothed);
let isPositiveSlope = false;
// Traverse the slope to detect where slope changes sharply
for (let i = 1; i < slope.length; i++) {
if (slope[i] > options.rPeakSlopeThreshold && !isPositiveSlope) {
// Start of a sharp positive slope
isPositiveSlope = true;
} else if (slope[i] < -options.rPeakSlopeThreshold && isPositiveSlope) {
// End of a sharp positive slope -> this is likely an R-peak
isPositiveSlope = false;
let j = i;
// Reverse search to find the peak
while (j > 0 && smoothed[j][1] < smoothed[j - 1][1]) {
j--;
}
if (rPeaks.length === 0 || j - rPeaks[rPeaks.length - 1][0] > options.rPeakMinDistance) {
rPeaks.push([j, seg[j]]); // Add the index as an R-peak
}
}
}
return rPeaks.map((val) => val[1]);
}
function getBeats(rPeaks, seg, frequency, options) {
let beats = [];
let lastBeat = null;
for (let i = 0; i < rPeaks.length; i++) {
let peak = rPeaks[i];
let start = i === 0 ? 0 : rPeaks[i - 1][0];
if (lastBeat && lastBeat.t.end_time > 0) {
start = lastBeat.t.end_time;
}
let end = rPeaks[i + 1] ? rPeaks[i + 1][0] : -1;
let left = seg.filter((val) => val[0] >= start && val[0] < peak[0]);
let right = seg.filter((val) => val[0] > peak[0] && ((end > 0 && val[0] <= end) || end < 0));
let leftSlope = calculateSlope(left);
let rightSlope = calculateSlope(right);
// Get qrs complex
//iterate left arm to find q wave start
// decide by the slope
let qWaveStartIndex = -1;
for (let j = leftSlope.length - 1; j >= 0; j--) {
if (Math.abs(leftSlope[j]) * frequency < 2.5) {
//45 degree
qWaveStartIndex = j;
break;
}
}
// console.log('qWaveStartIndex:', qWaveStartIndex);
let leftStartIdx = 0;
let smoothLeft = smoothSignal(left, options.smoothWindowSize);
let leftPeaks = findPeaks(smoothLeft, 1, options.throughMinDistance, options.minPWaveHeight);
if (leftPeaks.length > 0) {
for (let j = leftPeaks[0]; j > 0; j--) {
let slope = (smoothLeft[j][1] - smoothLeft[j - 1][1]) * frequency;
// console.log('leftSlope:', j, slope, left[j + 1][1], left[j][1]);
if (slope >= -0.05 && Math.abs(slope) < 2.5) {
leftStartIdx = j;
break;
}
}
}
if (leftStartIdx > 0) {
left = left.slice(leftStartIdx);
//adjust qWaveStartIndex because of the cut
qWaveStartIndex = qWaveStartIndex - leftStartIdx;
}
//cut the right from peak to second trough value
let smoothRight = smoothSignal(right, options.smoothWindowSize);
let rightEndIdx = -1;
let rWaveEndIdx = -1;
for (let j = 0; j < rightSlope.length; j++) {
// console.log('rightSlope1:', j, rightSlope[j], Math.abs(rightSlope[j]) * frequency);
if (Math.abs(rightSlope[j]) * frequency < 2.5) {
//45 degree
rWaveEndIdx = j;
break;
}
}
let rightPeaks = findPeaks(smoothRight, 1, options.throughMinDistance, options.minTWaveHeight);
// console.log('rWavePeak:', i, options.throughMinDistance);
// console.log('rightPeaks:', rightPeaks, right);
let tWaveEndTime = -1;
if (rightPeaks.length > 0) {
let peakIdx = 0;
while (rightPeaks[peakIdx] && rightPeaks[peakIdx] < rWaveEndIdx) {
peakIdx++;
}
for (let j = rightPeaks[peakIdx]; j < right.length - 1; j++) {
let slope = (smoothRight[j + 1][1] - smoothRight[j][1]) * frequency;
// console.log('rightSlope:', j, slope, smoothRight[j + 1][1], smoothRight[j][1]);
if (slope >= -0.05 && Math.abs(slope) < 2.5) {
rightEndIdx = j;
tWaveEndTime = right[j][0];
break;
}
}
}
if (rightEndIdx > 0) {
right = right.slice(0, rightEndIdx);
} else {
tWaveEndTime = right[right.length - 1][0];
}
let beat = {
_data: [].concat(left || [], [peak], right || []),
valid: false,
start_time: (left && left.length) ? left[0][0] : null,
end_time: right.length > 0 ? right[right.length - 1][0] : null,
p: {
start_time: left.length > 0 ? left[0][0] : 0,
end_time: 0,
peak_time: 0,
peak_voltage: 0
},
q: {
start_time: qWaveStartIndex > 0 ? left[qWaveStartIndex-1][0] : 0,
end_time: 0,
peak_time: 0,
peak_voltage: 0
},
r: {
start_time: 0,
end_time: 0,
peak_time: peak[0],
peak_voltage: peak[1]
},
s: {
start_time: 0,
end_time:( rWaveEndIdx >= 0&& right[rWaveEndIdx+1]) ? right[rWaveEndIdx+1][0] : 0,
peak_time: 0,
peak_voltage: 0
},
t: {
start_time: 0,
end_time: tWaveEndTime >= 0 ? tWaveEndTime : 0,
peak_time: 0,
peak_voltage: 0
}
};
beats.push(beat);
lastBeat = beat;
/*
try {
} catch (e) {
console.error('error:', e, minLeftItemIndex, left, peak, right);
}*/
}
return beats;
}
function updateWaves(beats, options) {
//iterate beats
for (let beat of beats) {
let rawData = beat._data;
let extendedRawData = Array.from(rawData);
const extension = 20;
for (let i = 0; i < extension; i++) {
extendedRawData.unshift(rawData[0]);
extendedRawData.push(rawData[rawData.length - 1]);
}
let smooth = smoothSignal(extendedRawData, options.smoothWindowSize);
let lastVal = null;
//baseline ignore qrs complex
let baseData = smooth.map((val) => {
if (val[0] >= beat.q.start_time && val[0] <= beat.s.end_time) {
return [val[0], lastVal];
} else {
if (typeof lastVal !== 'number') {
lastVal = val[1];
}
return val;
}
});
let baseline,
baselineWithoutQRS;
if (options.baselineFilter === 'LOWPASS') {
baselineWithoutQRS = lowPassFilter(baseData, options.baselineFilterOptions?.alpha);
baseline = lowPassFilter(smooth, options.baselineFilterOptions?.alpha);
} else if (options.baselineFilter === 'MEAN') {
baselineWithoutQRS = meanFilter(baseData, options.baselineFilterOptions?.windowSize);
baseline = meanFilter(smooth, options.baselineFilterOptions?.windowSize);
} else if (options.baselineFilter === 'MEDIAN') {
baselineWithoutQRS = medianFilter(baseData, options.baselineFilterOptions?.windowSize);
baseline = medianFilter(smooth, options.baselineFilterOptions?.windowSize);
} else {
throw new Error('Invalid baseline filter', typeof options.baselineFilter, options.baselineFilter, 'is not a valid filter function');
}
// cutoff the extended data
smooth.splice(0, extension);
smooth.splice(smooth.length - extension, extension);
baseline.splice(0, extension);
baseline.splice(baseline.length - extension, extension);
baselineWithoutQRS.splice(0, extension);
baselineWithoutQRS.splice(baselineWithoutQRS.length - extension, extension);
beat._baseline = baseline;
beat._baselineWithoutQRS = baselineWithoutQRS;
let rPeakIndex = rawData.findIndex((val) => val[0] === beat.r.peak_time);
let left = rawData.slice(0, rPeakIndex + 1);
let right = rawData.slice(rPeakIndex);
let smoothLeft = smooth.slice(0, rPeakIndex + 1)
let smoothRight = smooth.slice(rPeakIndex);
let qWaveStartIndex = left.findIndex((val) => val[0] === beat.q.start_time);
let sWaveEndIndex = right.findIndex((val) => val[0] === beat.s.end_time);
//get baseline voltage
let baselineLeft = [];
let baselineRight = [];
let baselineWithoutQRSLeft = [];
let baselineWithoutQRSRight = [];
baselineLeft = baseline.slice(0, rPeakIndex + 1);
baselineRight = baseline.slice(rPeakIndex);
baselineWithoutQRSLeft = baselineWithoutQRS.slice(0, rPeakIndex + 1);
baselineWithoutQRSRight = baselineWithoutQRS.slice(rPeakIndex);
// console.log('baseVoltageLeft:', baselineLeft);
// console.log('smoothLeft:', smoothLeft, leftTroughs, qPeakIndex);
// console.log('baseVoltageRight:', baselineRight);
// console.log('smoothRight:', smoothRight, rightTroughs, sPeakIndex);
const threshold = 1 / 30;
//get p wave
let pWaveEndIndex = -1,
pWaveStartIndex = -1,
pWaveProbablyStartIndex = rawData.findIndex((val) => val[0] === beat.p.start_time);
for (let i = pWaveProbablyStartIndex; i < qWaveStartIndex; i++) {
if (smoothLeft[i] && smoothLeft[i - 1] && smoothLeft[i][1] > smoothLeft[i - 1][1] && smoothLeft[i - 1][1] <= baselineLeft[i - 1][1] + threshold && smoothLeft[i][1] >= baselineLeft[i][1] + threshold) {
pWaveStartIndex = i;
break;
}
}
for (let i = qWaveStartIndex; i > 0; i--) {
if (smoothLeft[i] && smoothLeft[i + 1] && smoothLeft[i][1] > smoothLeft[i + 1][1]) {
if (smoothLeft[i][1] >= baselineLeft[i][1] && smoothLeft[i][1] <= baselineWithoutQRSLeft[i][1] + threshold && smoothLeft[i + 1][1] <= baselineWithoutQRSLeft[i + 1][1] + threshold) {
pWaveEndIndex = i;
break;
}
}
}
options.debug && console.log('pWaveStartIndex:', pWaveStartIndex, 'pWaveEndIndex:', pWaveEndIndex);
if (pWaveEndIndex >= 0 && pWaveStartIndex >= 0 && pWaveStartIndex < pWaveEndIndex) {
beat.p.start_time = left[pWaveStartIndex][0];
beat.p.end_time = left[pWaveEndIndex][0];
beat.p._data = left.slice(pWaveStartIndex, pWaveEndIndex);
//get p wave peak
let pWavePeakValue = Math.max.apply(null, smoothLeft.slice(pWaveStartIndex, pWaveEndIndex).map((val) => val[1]));
let pWavePeakIndex = smoothLeft.findIndex((val) => val[1] === pWavePeakValue);
options.debug && console.log('pWavePeakIndex:', pWavePeakIndex, pWavePeakValue, smoothLeft.slice(pWaveStartIndex, pWaveEndIndex).map((val) => val[1]));
beat.p.peak_time = left[pWavePeakIndex][0];
beat.p.peak_voltage = left[pWavePeakIndex][1];
}
//get q wave
let qWavePeakIndex = -1;
let qWaveEndIndex = -1;
//iterate left find q wave end
// let minSlope = Math.min.apply(null, slopeLeft);
for (let i = smoothLeft.length - 1; i >= qWaveStartIndex; i--) {
if (smoothLeft[i] && smoothLeft[i + 1] && smoothLeft[i][1] < smoothLeft[i + 1][1]) {
// console.log('qWaveEndIndex:', i, smoothLeft[i][1], smoothLeft[i + 1][1], 'baseline:', baselineLeft[i][1], baselineLeft[i + 1][1]);
// console.log('qWaveEndIndex:', i, smoothLeft[i][1], smoothLeft[i + 1][1], 'baseline:', baselineLeft[i][1], baselineLeft[i + 1][1]);
if (smoothLeft[i][1] <= baselineLeft[i][1] + threshold && smoothLeft[i + 1][1] >= baselineLeft[i + 1][1] + threshold) {
qWaveEndIndex = i;
break;
}
}
}
let qWaveThroughValue = Math.min.apply(null, smoothLeft.slice(qWaveStartIndex, qWaveEndIndex).map((val) => val[1]));
qWavePeakIndex = smoothLeft.findLastIndex((val) => val[1] === qWaveThroughValue);
options.debug && console.log('qWaveStartIndex:', qWaveStartIndex, 'qWavePeakIndex:', qWavePeakIndex, 'qWaveEndIndex:', qWaveEndIndex);
if (qWaveEndIndex >= 0 && qWaveStartIndex >= 0 && qWaveStartIndex <= qWavePeakIndex) {
beat.q.start_time = left[qWaveStartIndex][0];
beat.q.end_time = left[qWaveEndIndex][0];
beat.q._data = left.slice(qWaveStartIndex, qWaveEndIndex);
beat.q.peak_time = left[qWavePeakIndex][0];
beat.q.peak_voltage = left[qWavePeakIndex][1];
}
//get r wave
let rWaveStartIndex = qWaveEndIndex;
let rWaveEndIndex = -1;
//iterate right find r wave end
for (let i = 1; i < sWaveEndIndex; i++) {
console.log('rWaveEndIndex:', i, smoothRight[i][1], smoothRight[i - 1][1], baselineRight[i][1],threshold, baselineRight[i - 1][1],baselineWithoutQRSRight[i][1], baselineWithoutQRSRight[i - 1][1]);
console.log('beat',beat)
if (smoothRight[i] && smoothRight[i - 1] && smoothRight[i-1][1] >= smoothRight[i][1] && smoothRight[i][1] <= baselineRight[i][1]+threshold) {
rWaveEndIndex = i;
break;
}
}
options.debug && console.log('rWaveStartIndex:', rWaveStartIndex, 'rWaveEndIndex:', rWaveEndIndex);
if (rWaveStartIndex >= 0 && rWaveEndIndex >= 0 && rWaveEndIndex < sWaveEndIndex) {
beat.r.start_time = left[rWaveStartIndex][0];
beat.r.end_time = right[rWaveEndIndex][0];
beat.r._data = [].concat(left.slice(rWaveStartIndex), right.slice(0, rWaveEndIndex));
}
//get s wave
let sWaveStartIndex = rWaveEndIndex;
let sWavePeakIndex = -1;
let sWaveThroughValue = Math.min.apply(null, smoothRight.slice(sWaveStartIndex, sWaveEndIndex).map((val) => val[1]));
sWavePeakIndex = smoothRight.findIndex((val) => val[1] === sWaveThroughValue);
options.debug && console.log('sWaveStartIndex:', sWaveStartIndex, 'sWavePeakIndex:', sWavePeakIndex, 'sWaveEndIndex:', sWaveEndIndex);
if (sWaveStartIndex >= 0 && sWaveEndIndex >= 0 && sWavePeakIndex >= 0 && sWaveEndIndex >= sWavePeakIndex) {
beat.s.start_time = right[sWaveStartIndex][0];
beat.s.end_time = right[sWaveEndIndex][0];
beat.s._data = right.slice(sWaveStartIndex, sWaveEndIndex);
beat.s.peak_time = right[sWavePeakIndex][0];
beat.s.peak_voltage = right[sWavePeakIndex][1];
}
//get t wave
let tWaveStartIndex = -1;
let tWavePeakIndex = null;
let tWaveEndIndex = right.findIndex((val) => val[0] === beat.t.end_time);
if (tWaveEndIndex < 0) {
tWaveEndIndex = right.length - 1;
}
//iterate right find t wave end
for (let i = sWaveEndIndex; i < right.length; i++) {
if (smoothRight[i] && smoothRight[i - 1] && smoothRight[i][1] > smoothRight[i - 1][1] && smoothRight[i][1] >= baselineWithoutQRSRight[i][1] + threshold) {
tWaveStartIndex = i;
break;
}
}
/* for (let i = right.length - 1; i >= 0; i--) {
if (smoothRight[i] && smoothRight[i + 1] && smoothRight[i][1] > smoothRight[i + 1][1]) {
tWaveEndIndex = i;
break;
}
}*/
options.debug && console.log('tWaveStartIndex:', tWaveStartIndex, 'tWavePeakIndex:', tWavePeakIndex, 'tWaveEndIndex:', tWaveEndIndex, beat.t.end_time, smoothRight, baselineRight);
if (tWaveStartIndex >= 0 && tWaveEndIndex >= 0 && tWaveStartIndex < tWaveEndIndex) {
beat.t.start_time = right[tWaveStartIndex][0];
beat.t.end_time = right[tWaveEndIndex][0];
beat.t._data = right.slice(tWaveStartIndex, tWaveEndIndex);
//get t wave peak
let tWavePeakValue = Math.max.apply(null, right.slice(tWaveStartIndex, tWaveEndIndex).map((val) => val[1]));
let tWavePeakIndex = right.findLastIndex((val) => val[1] >= tWavePeakValue);
options.debug && console.log('tWavePeakIndex:', tWavePeakIndex, tWavePeakValue, right);
beat.t.peak_time = right[tWavePeakIndex][0];
beat.t.peak_voltage = right[tWavePeakIndex][1];
}
}
return beats;
}
function testBeats(beats) {
for (let beat of beats) {
let p,
q,
r,
s,
t;
p = beat.p.start_time >= 0 && beat.p.end_time >= 0 && beat.p.peak_time > 0;
q = beat.q.start_time >= 0 && beat.q.end_time >= 0 && beat.q.peak_time > 0;
r = beat.r.start_time >= 0 && beat.r.end_time >= 0 && beat.r.peak_time > 0;
s = beat.s.start_time >= 0 && beat.s.end_time >= 0 && beat.s.peak_time > 0;
t = beat.t.start_time >= 0 && beat.t.end_time >= 0 && beat.t.peak_time > 0;
beat.valid = p && q && r && s && t;
}
}
function getSummary(beats, frequency = 100, options) {
let summary = {
hr: null,
st: 0,
pr: 0,
qrs: 0,
};
//get heart rate
if (beats.length >= 2) {
let duration = beats[beats.length - 1].r.peak_time - beats[0].r.peak_time;
if (duration > 0) {
summary.hr_duration = duration;
if (options.useDirectData) {
//when use direct data, the duration is in 1/frequency second
summary.hr_duration = duration * (1000 / frequency);
}
summary.hr = Math.round(60 / (duration / 1000 / (beats.length - 1)));
}
summary.hr_beats = beats.length - 1;
}
//get st ,pr and QRS
let st = [],
pr = [],
qrs = [];
let validBeats = 0;
for (let beat of beats) {
if (beat.valid) {
let stValid = false;
if (beat.s.end_time > 0) {
let jPointIdx = beat._data.findIndex((val) => val[0] === beat.s.end_time);
let tStartIdx = beat._data.findIndex((val) => val[0] === beat.t.start_time);
let jPointVoltage = beat._data[jPointIdx][1];
if (tStartIdx > jPointIdx + 1 && beat._data[jPointIdx + 1] && beat._data[jPointIdx + 1][1] > jPointVoltage) {
jPointVoltage = beat._data[jPointIdx + 1][1];
}
let _80msAfterJPoint = Math.min(jPointIdx + Math.floor(0.08 * frequency), tStartIdx);
if (beat._data[_80msAfterJPoint]) {
let _80msVoltage = beat._data[_80msAfterJPoint][1];
// let _80msBaseline = beat._baseline[_80msAfterJPoint][1];
// console.log('ST Voltage:', _80msVoltage - jPointVoltage,beat);
st.push(_80msVoltage - jPointVoltage);
stValid = true;
}
}
if (stValid) {
validBeats++
let pr_time = (beat.r.start_time - beat.p.start_time);
let qrs_time = (beat.s.end_time - beat.q.start_time);
if (options.useDirectData) {
//when use direct data, the duration is in 1/frequency second
pr_time = pr_time * 1000 / frequency
qrs_time = qrs_time * 1000 / frequency
}
pr.push(pr_time);
qrs.push(qrs_time);
} else {
beat.valid = false;
}
}
}
if (st.length > 0) {
summary.st = st.reduce((a, b) => a + b) / st.length;
} else {
summary.st = null;
}
if (pr.length > 0) {
summary.pr = pr.reduce((a, b) => a + b) / pr.length;
}
if (qrs.length > 0) {
summary.qrs = qrs.reduce((a, b) => a + b) / qrs.length;
}
summary.validBeats = validBeats;
return summary;
}
function getSummaryFromSegments(segments) {
let summary = {
hr: null,
st: 0,
pr: 0,
qrs: 0,
};
let hrSum = 0,
hrBeats = 0;
let stSum = 0,
prSum = 0,
qrsSum = 0;
let validBeats = 0,
stBeats = 0;
for (let seg of segments) {
if (seg.summary.hr_duration) {
hrSum += seg.summary.hr_duration;
hrBeats += seg.summary.hr_beats;
}
if (typeof seg.summary.st === 'number') {
stSum += seg.summary.st * seg.summary.validBeats;
stBeats++;
}
if (seg.summary.pr) {
prSum += seg.summary.pr * seg.summary.validBeats;
}
if (seg.summary.qrs) {
qrsSum += seg.summary.qrs * seg.summary.validBeats;
}
validBeats += seg.summary.validBeats;
}
if (hrBeats > 0) {
summary.hr = Math.round(60 / (hrSum / 1000 / hrBeats));
}
if (validBeats > 0) {
summary.pr = prSum / validBeats;
summary.qrs = qrsSum / validBeats;
}
if (stBeats > 0) {
summary.st = stSum / stBeats;
} else {
summary.st = null;
}
return summary;
}
options = Object.assign({
debug: false,
aggregation: null,
rPeakSlopeThreshold: 0.05,
rPeakMinDistance: 30,
smoothWindowSize: 5,
baselineFilter: 'LOWPASS',
baselineFilterOptions: {
alpha: 0.05,
windowSize: 20
},
throughMinDistance: 10,
minPWaveHeight: 0,
minTWaveHeight: 0.1,
useDirectData: false,
}, options || {});
frequency = frequency || 100;
//declare resultObj
let aggregationRatio = options.aggregation || Math.floor(frequency / 100);
if (!options.useDirectData) {
ecgData = aggregation(ecgData, aggregationRatio);
frequency = frequency / aggregationRatio;
}
let resultObj = new resultConstructor(ecgData, frequency, options);
ecgData = resultObj._data;
//splite data to segments
let segments = splitData(ecgData);
// console.log('segments:', segments);
//iterate segments
for (let seg of segments) {
//get r-peaks
let rPeaks = getRPeaks(seg, options);
//get beats
let beats = getBeats(rPeaks, seg, frequency, options);
//wave of beats
updateWaves(beats, options);
//test beats is valid
testBeats(beats);
//get summary of beats
let summary = getSummary(beats, frequency, options);
resultObj._result.segments.push({
start_time: seg[0][0],
end_time: seg[seg.length - 1][0],
summary: summary,
beats: beats
});
}
//get summary of all segments
resultObj._result.summary = getSummaryFromSegments(resultObj._result.segments);
return resultObj;
}
try {
module.exports = {
filters: {
'LOWPASS': 'LOWPASS',
'MEAN': 'MEAN',
'MEDIAN': 'MEDIAN'
},
process: ecg_data_process_v1
};
} catch (e) {
console.error("module.exports is not defined");
if (typeof global === 'undefined') {
var global = window;
}
global.ecg_data_process = {
filters: {
'LOWPASS': 'LOWPASS',
'MEAN': 'MEAN',
'MEDIAN': 'MEDIAN'
},
process: ecg_data_process_v1
}
}