UNPKG

dozee-ecg-chart

Version:

A library for drawing ECG charts with real-time data

256 lines (249 loc) 11.2 kB
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