dozee-ecg-chart
Version:
A library for drawing ECG charts with real-time data
256 lines (249 loc) • 11.2 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Component, Input, NgModule } from '@angular/core';
import { Chart, CategoryScale, LinearScale, LineController, LineElement, PointElement, Title, Tooltip, Legend } from 'chart.js';
import { CommonModule } from '@angular/common';
class DozeeEcgChartService {
constructor() { }
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.3", ngImport: i0, type: DozeeEcgChartService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.3", ngImport: i0, type: DozeeEcgChartService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.3", ngImport: i0, type: DozeeEcgChartService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
class DozeeEcgChartComponent {
renderer;
ngZone;
accessToken;
userId;
stage;
strokeColor = '#00ff00';
backgroundColor = '#000000';
eventSource = null;
buffer = [];
chart;
frequency = 256; // Hz
duration = 8; // seconds
maxPoints = this.frequency * this.duration; // 2048 points
ecgData = Array(this.maxPoints).fill(null);
bufferLimit = 64 * 64;
isPageActive = true;
intervalId;
inactivityTimeout; // Timer to track inactivity
INACTIVE_DELAY = 30000;
RETRY_DELAY = 6000;
constructor(renderer, ngZone) {
this.renderer = renderer;
this.ngZone = ngZone;
}
ngOnInit() {
// Register necessary chart components
Chart.register(CategoryScale, LinearScale, LineController, LineElement, PointElement, Title, Tooltip, Legend);
const ctx = document.getElementById('ecgChart');
ctx.style.backgroundColor = this.backgroundColor;
this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: Array(this.maxPoints).fill(0.0), // Empty labels as X axis is fixed
datasets: [
{
label: 'ECG Data',
data: this.ecgData,
borderColor: this.strokeColor,
borderWidth: 1,
fill: false,
pointRadius: 0, // Hide points for smooth line
},
],
},
options: {
animation: false,
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
min: 0,
max: this.maxPoints, // Set fixed X-axis range (8 seconds)
display: false, // Hide the x-axis
grid: { display: false },
},
y: {
max: 4000, // Adjust based on ECG value range
min: 0,
type: 'linear',
beginAtZero: true,
grid: { display: false },
display: false,
},
},
plugins: {
legend: {
display: false,
},
},
elements: {
line: {
tension: 0.4,
},
},
},
});
// Initialize SSE connection
this.initializeSse();
this.renderer.listen('document', 'visibilitychange', () => {
if (document.hidden) {
console.log('Page became inactive, starting 30s timeout...');
this.startInactivityTimer();
}
else {
console.log('Page is active again, canceling inactivity timeout');
this.cancelInactivityTimer();
this.startInterval();
}
});
// Start interval initially
this.startInterval();
}
startInterval() {
if (!this.intervalId) {
// Update chart data at regular intervals
this.intervalId = setInterval(() => {
if (this.buffer.length > this.bufferLimit && this.currentIndex === 0) {
if (this.stage === 'sit') {
console.warn(`Buffer overflow: Skipping ${this.buffer.length - 64} old entries`);
}
this.buffer.splice(0, this.buffer.length - 64);
}
if (this.buffer.length > 0) {
const entry = this.buffer.shift();
for (const e of entry) {
this.addData(e);
}
}
}, 16); // Update frequency (256 Hz)
}
}
startInactivityTimer() {
this.inactivityTimeout = setTimeout(() => {
console.log('Page has been inactive for 30 seconds, stopping updates.');
this.isPageActive = false;
this.clearInterval();
}, this.INACTIVE_DELAY);
}
cancelInactivityTimer() {
if (this.inactivityTimeout) {
clearTimeout(this.inactivityTimeout);
this.inactivityTimeout = null;
}
this.isPageActive = true;
}
clearInterval() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
currentIndex = 0;
addData(e) {
if (this.chart.data.labels) {
this.ecgData[this.currentIndex] = e.Value;
this.chart.data.labels[this.currentIndex] = this.currentIndex;
for (let i = this.currentIndex + 1; i < Math.min(this.currentIndex + 64, this.maxPoints); i++) {
this.ecgData[i] = null;
}
// Advance the index, and wrap around if it exceeds maxPoints
this.currentIndex = (this.currentIndex + 1) % this.maxPoints;
if (this.stage === 'sit') {
console.log('Current index:', this.currentIndex);
}
// Update the chart data
this.chart.data.datasets[0].data = this.ecgData;
this.chart.update();
}
}
initializeSse() {
const sseUrl = `https://ecgsse${this.stage ? `-${this.stage}` : ''}.dozee.cloud/sse/ecgstream?userId=${this.userId}&accessToken=${this.accessToken}&ngsw-bypass=true`;
this.ngZone.runOutsideAngular(() => {
this.eventSource = new EventSource(sseUrl);
this.eventSource.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.Key === 'SIGNAL' && this.isPageActive) {
this.ngZone.run(() => {
// Angular will detect this change and update bindings
let entry = [];
data.Values.forEach((s, i) => {
entry.push({
Timestamp: data.Timestamp + i * (1000 / this.frequency), // Adjust timestamp for frequency
Value: s,
});
if (entry.length === 4) {
this.buffer.push(entry);
if (this.stage === 'sit') {
console.log('Buffer length:', this.buffer.length);
}
entry = [];
}
});
});
}
};
this.eventSource.onerror = (error) => {
console.error('SSE error, will retry in 6s', error);
this.closeConnection();
// schedule a reconnect
setTimeout(() => {
this.initializeSse();
}, this.RETRY_DELAY);
};
});
}
ngOnDestroy() {
this.closeConnection();
}
closeConnection() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.3", ngImport: i0, type: DozeeEcgChartComponent, deps: [{ token: i0.Renderer2 }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.3", type: DozeeEcgChartComponent, selector: "app-ecg-chart", inputs: { accessToken: "accessToken", userId: "userId", stage: "stage", strokeColor: "strokeColor", backgroundColor: "backgroundColor" }, ngImport: i0, template: "<div class=\"chart-container\">\n <canvas id=\"ecgChart\"></canvas>\n</div>\n", styles: [".chart-container{width:100%;height:100%;position:relative}canvas{width:100%!important;height:100%!important}\n"] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.3", ngImport: i0, type: DozeeEcgChartComponent, decorators: [{
type: Component,
args: [{ selector: 'app-ecg-chart', template: "<div class=\"chart-container\">\n <canvas id=\"ecgChart\"></canvas>\n</div>\n", styles: [".chart-container{width:100%;height:100%;position:relative}canvas{width:100%!important;height:100%!important}\n"] }]
}], ctorParameters: () => [{ type: i0.Renderer2 }, { type: i0.NgZone }], propDecorators: { accessToken: [{
type: Input
}], userId: [{
type: Input
}], stage: [{
type: Input
}], strokeColor: [{
type: Input
}], backgroundColor: [{
type: Input
}] } });
class DozeeEcgChartModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.3", ngImport: i0, type: DozeeEcgChartModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.3", ngImport: i0, type: DozeeEcgChartModule, declarations: [DozeeEcgChartComponent], imports: [CommonModule], exports: [DozeeEcgChartComponent] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.3", ngImport: i0, type: DozeeEcgChartModule, imports: [CommonModule] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.3", ngImport: i0, type: DozeeEcgChartModule, decorators: [{
type: NgModule,
args: [{
declarations: [DozeeEcgChartComponent], // Declare the component
imports: [CommonModule], // Import necessary modules
exports: [DozeeEcgChartComponent], // Export the component so it can be used outside
}]
}] });
/*
* Public API Surface of dozee-ecg-chart
*/
/**
* Generated bundle index. Do not edit.
*/
export { DozeeEcgChartComponent, DozeeEcgChartModule, DozeeEcgChartService };
//# sourceMappingURL=dozee-ecg-chart.mjs.map