dcmjs-ecg
Version:
DICOM electrocardiography (ECG) rendering for Node.js and browser using dcmjs
2 lines • 17.7 kB
JavaScript
/*! dcmjs-ecg - 0.0.15 - 2026-05-19 | (c) 2022-2026 Pantelis Georgiadis | https://github.com/PantelisGeorgiadis/dcmjs-ecg */
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("dcmjs")):"function"==typeof define&&define.amd?define("dcmjs-ecg",["dcmjs"],t):"object"==typeof exports?exports["dcmjs-ecg"]=t(require("dcmjs")):e.dcmjsEcg=t(e.dcmjs)}(this,(e=>{return t={935(e,t,n){var r,o;r=function(e){"use strict";var t,n,r=function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(t in arguments[n])Object.prototype.hasOwnProperty.call(arguments[n],t)&&(e[t]=arguments[n][t]);return e},o={template:"[%t] %l:",levelFormatter:function(e){return e.toUpperCase()},nameFormatter:function(e){return e||"root"},timestampFormatter:function(e){return e.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/,"$1")},format:void 0},i={},a=function(e,n){if(!e||!e.setLevel)throw new TypeError("Argument is not a logger");var a=e.methodFactory,l=e.name||"",s=i[l]||i[""]||o;function c(e,t,n){var r=a(e,t,n),o=i[n]||i[""],s=-1!==o.template.indexOf("%t"),c=-1!==o.template.indexOf("%l"),u=-1!==o.template.indexOf("%n");return function(){for(var t="",a=arguments.length,f=Array(a),d=0;d<a;d++)f[d]=arguments[d];if(l||!i[n]){var p=o.timestampFormatter(new Date),h=o.levelFormatter(e),m=o.nameFormatter(n);o.format?t+=o.format(h,m,p):(t+=o.template,s&&(t=t.replace(/%t/,p)),c&&(t=t.replace(/%l/,h)),u&&(t=t.replace(/%n/,m))),f.length&&"string"==typeof f[0]?f[0]=t+" "+f[0]:f.unshift(t)}r.apply(void 0,f)}}return i[l]||(e.methodFactory=c),(n=n||{}).template&&(n.format=void 0),i[l]=r({},s,n),e.setLevel(e.getLevel()),t||e.warn("It is necessary to call the function reg() of loglevel-plugin-prefix before calling apply. From the next release, it will throw an error. See more: https://github.com/kutuluk/loglevel-plugin-prefix/blob/master/README.md"),e},l={reg:function(e){if(!e||!e.getLogger)throw new TypeError("Argument is not a root logger");t=e},apply:a};return e&&(n=e.prefix,l.noConflict=function(){return e.prefix===l&&(e.prefix=n),l}),l},void 0===(o="function"==typeof r?r.call(t,n,t,e):r)||(e.exports=o)},213(e,t,n){var r,o;!function(){"use strict";r=function(){var e=function(){},t="undefined",n=typeof window!==t&&typeof window.navigator!==t&&/Trident\/|MSIE /.test(window.navigator.userAgent),r=["trace","debug","info","warn","error"],o={},i=null;function a(e,t){var n=e[t];if("function"==typeof n.bind)return n.bind(e);try{return Function.prototype.bind.call(n,e)}catch(t){return function(){return Function.prototype.apply.apply(n,[e,arguments])}}}function l(){console.log&&(console.log.apply?console.log.apply(console,arguments):Function.prototype.apply.apply(console.log,[console,arguments])),console.trace&&console.trace()}function s(r){return"debug"===r&&(r="log"),typeof console!==t&&("trace"===r&&n?l:void 0!==console[r]?a(console,r):void 0!==console.log?a(console,"log"):e)}function c(){for(var n=this.getLevel(),o=0;o<r.length;o++){var i=r[o];this[i]=o<n?e:this.methodFactory(i,n,this.name)}if(this.log=this.debug,typeof console===t&&n<this.levels.SILENT)return"No console available for logging"}function u(e){return function(){typeof console!==t&&(c.call(this),this[e].apply(this,arguments))}}function f(e,t,n){return s(e)||u.apply(this,arguments)}function d(e,n){var a,l,s,u=this,d="loglevel";function p(e){var n=(r[e]||"silent").toUpperCase();if(typeof window!==t&&d){try{return void(window.localStorage[d]=n)}catch(e){}try{window.document.cookie=encodeURIComponent(d)+"="+n+";"}catch(e){}}}function h(){var e;if(typeof window!==t&&d){try{e=window.localStorage[d]}catch(e){}if(typeof e===t)try{var n=window.document.cookie,r=encodeURIComponent(d),o=n.indexOf(r+"=");-1!==o&&(e=/^([^;]+)/.exec(n.slice(o+r.length+1))[1])}catch(e){}return void 0===u.levels[e]&&(e=void 0),e}}function m(){if(typeof window!==t&&d){try{window.localStorage.removeItem(d)}catch(e){}try{window.document.cookie=encodeURIComponent(d)+"=; expires=Thu, 01 Jan 1970 00:00:00 UTC"}catch(e){}}}function g(e){var t=e;if("string"==typeof t&&void 0!==u.levels[t.toUpperCase()]&&(t=u.levels[t.toUpperCase()]),"number"==typeof t&&t>=0&&t<=u.levels.SILENT)return t;throw new TypeError("log.setLevel() called with invalid level: "+e)}"string"==typeof e?d+=":"+e:"symbol"==typeof e&&(d=void 0),u.name=e,u.levels={TRACE:0,DEBUG:1,INFO:2,WARN:3,ERROR:4,SILENT:5},u.methodFactory=n||f,u.getLevel=function(){return null!=s?s:null!=l?l:a},u.setLevel=function(e,t){return s=g(e),!1!==t&&p(s),c.call(u)},u.setDefaultLevel=function(e){l=g(e),h()||u.setLevel(e,!1)},u.resetLevel=function(){s=null,m(),c.call(u)},u.enableAll=function(e){u.setLevel(u.levels.TRACE,e)},u.disableAll=function(e){u.setLevel(u.levels.SILENT,e)},u.rebuild=function(){if(i!==u&&(a=g(i.getLevel())),c.call(u),i===u)for(var e in o)o[e].rebuild()},a=g(i?i.getLevel():"WARN");var y=h();null!=y&&(s=g(y)),c.call(u)}(i=new d).getLogger=function(e){if("symbol"!=typeof e&&"string"!=typeof e||""===e)throw new TypeError("You must supply a name when creating a logger.");var t=o[e];return t||(t=o[e]=new d(e,i.methodFactory)),t};var p=typeof window!==t?window.log:void 0;return i.noConflict=function(){return typeof window!==t&&window.log===i&&(window.log=p),i},i.getLoggers=function(){return o},i.default=i,i},void 0===(o="function"==typeof r?r.call(t,n,t,e):r)||(e.exports=o)}()},688(e,t,n){const r=n(880),o=n(547),i=n(111),{DicomMetaDictionary:a,DicomMessage:l,ReadBufferStream:s,WriteBufferStream:c}=i.data,u=i.log,f=["1.2.840.10008.5.1.4.1.1.9.1.1","1.2.840.10008.5.1.4.1.1.9.1.2","1.2.840.10008.5.1.4.1.1.9.1.3","1.2.840.10008.5.1.4.1.1.9.2.1","1.2.840.10008.5.1.4.1.1.9.3.1"];Object.freeze(f),Object.freeze(96);const d=96/25.4;Object.freeze(d);const p={DefaultSpeed:25,DefaultAmplitude:5,DefaultPaperBackgroundColor:{r:255,g:255,b:255,a:255},DefaultGridBackgroundColor:{r:210,g:210,b:210,a:255},DefaultGridForegroundColor:{r:227,g:69,b:56,a:175},DefaultSignalColor:{r:0,g:0,b:0,a:255},DefaultTextColor:{r:0,g:0,b:0,a:255}};Object.freeze(p),e.exports=class{constructor(e,t){if(u.level="error",this.transferSyntaxUid=t||"1.2.840.10008.1.2",e instanceof ArrayBuffer)if(t)this.elements=this._fromElementsBuffer(e,t);else{const t=this._fromP10Buffer(e);this.elements=t.elements,this.transferSyntaxUid=t.transferSyntaxUid}else this.elements=e||{}}getElement(e){return this.elements[e]}setElement(e,t){this.elements[e]=t}getElements(){return this.elements}getTransferSyntaxUid(){return this.transferSyntaxUid}setTransferSyntaxUid(e){this.transferSyntaxUid=e}getDenaturalizedDataset(e,t){const n=t?a.denaturalizeDataset(this.getElements(),{...a.nameMap,...t}):a.denaturalizeDataset(this.getElements()),r=new c;return l.write(n,r,this.transferSyntaxUid,e),r.getBuffer()}render(e){return(e=e||{}).speed=e.speed||p.DefaultSpeed,e.amplitude=e.amplitude||p.DefaultAmplitude,e.applyLowPassFilter=e.applyLowPassFilter||!1,this._render(e)}toString(){const e=[];return e.push("DICOM ECG:"),e.push("=".repeat(50)),e.push(JSON.stringify(this.getElements())),e.join("\n")}_fromP10Buffer(e){const t=l.readFile(e,{ignoreErrors:!0}),n=a.naturalizeDataset(t.meta).TransferSyntaxUID;return{elements:a.naturalizeDataset(t.dict),transferSyntaxUid:n}}_fromElementsBuffer(e,t){const n=new s(e);let r="1.2.840.10008.1.2"===t?"1.2.840.10008.1.2":"1.2.840.10008.1.2.1";const o=l._read(n,r,{ignoreErrors:!0});return a.naturalizeDataset(o)}_render(e){const t=this.getElement("SOPClassUID");if(!f.includes(t))throw new Error(`SOP class UID is not supported [${t}]`);const n=this._extractWaveform(e),o=this._extractInformation(n),i=this._extractAnnotation();i.length>0&&o.push({key:"Annotation",value:i}),o.push({key:"Sampling Frequency",value:n.samplingFrequency,unit:"Hz"}),o.push({key:"Duration",value:n.samples/n.samplingFrequency,unit:"sec"}),o.push({key:"Speed",value:e.speed,unit:"mm/sec"}),o.push({key:"Amplitude",value:e.amplitude,unit:"mm/mV"});const a=n.leads.length,l=Math.trunc(e.speed*d*n.samples/n.samplingFrequency),s=Math.trunc(e.amplitude*d*Math.ceil(n.minMax)*2*a),c=Math.trunc(s/a),u=new r(l,s,p.DefaultPaperBackgroundColor);for(let e=0;e<a;e++)this._renderLeadGrid(u,e,l,c),this._renderLeadSignal(u,n,n.leads[e],e,l,c),this._renderLeadTitle(u,n.leads[e],e,c);return o.push({key:"Width",value:l}),o.push({key:"Height",value:s}),{svg:u.toXmlString(),info:o}}_extractWaveform(e){const t=this.getElement("WaveformSequence");if(void 0===t||!Array.isArray(t)||0===t.length)throw new Error("WaveformSequence is empty");const n=t.find((e=>e));if(!["SS","SB","UB"].includes(n.WaveformSampleInterpretation))throw new Error(`Waveform sample interpretation is not supported [${n.WaveformSampleInterpretation}]`);if(8!==n.WaveformBitsAllocated&&16!==n.WaveformBitsAllocated)throw new Error(`Waveform bits allocated is not supported [${n.WaveformBitsAllocated}]`);const r={channelDefinitionSequence:n.ChannelDefinitionSequence,waveformData:n.WaveformData,channels:n.NumberOfWaveformChannels,samples:n.NumberOfWaveformSamples,samplingFrequency:n.SamplingFrequency,duration:n.NumberOfWaveformSamples/n.SamplingFrequency,bitsAllocated:n.WaveformBitsAllocated,sampleInterpretation:n.WaveformSampleInterpretation};return this._calculateLeads(r,e),this._sortLeads(r),r}_calculateLeads(e,t){const n=e.channelDefinitionSequence;if(void 0===n||!Array.isArray(n)||0===n.length)throw new Error("ChannelDefinitionSequence is empty");e.channels!==n.length&&o.warn(`Waveform number of channels [${e.channels}] are not equal to channel definition sequence length [${n.length}]. Proceeding with channel definition sequence length.`);const r=n.length,i=new Array(r).fill(1),a=new Array(r).fill(0),l=[],s=[];n.forEach(((e,t)=>{if(void 0!==e){if(8!==e.WaveformBitsStored&&16!==e.WaveformBitsStored)throw new Error(`Waveform bits stored definition is not supported [${e.WaveformBitsStored}]`);void 0!==e.ChannelSensitivity&&void 0!==e.ChannelSensitivityCorrectionFactor&&(i[t]=e.ChannelSensitivity*e.ChannelSensitivityCorrectionFactor),void 0!==e.ChannelBaseline&&(a[t]=e.ChannelBaseline);const n=e.ChannelSensitivityUnitsSequence;if(void 0!==n&&Array.isArray(n)&&n.length>0){const e=n[0];void 0!==e.CodeValue&&l.push(e.CodeValue)}const r=e.ChannelSourceSequence;void 0!==r&&Array.isArray(r)&&0!==r.length&&r.forEach((e=>{let t=void 0!==e.CodeMeaning?e.CodeMeaning:"",n=e.CodeValue;const r=e.CodingSchemeDesignator;if(!t&&void 0!==n&&void 0!==r){n=n.replace(/[^0-9-:.]/g,"");const e=[{mdcCode:"2:1",scpEcgCode:"5.6.3-9-1",title:"I"},{mdcCode:"2:2",scpEcgCode:"5.6.3-9-2",title:"II"},{mdcCode:"2:61",scpEcgCode:"5.6.3-9-61",title:"III"},{mdcCode:"2:62",scpEcgCode:"5.6.3-9-62",title:"aVR"},{mdcCode:"2:63",scpEcgCode:"5.6.3-9-63",title:"aVL"},{mdcCode:"2:64",scpEcgCode:"5.6.3-9-64",title:"aVF"},{mdcCode:"2:3",scpEcgCode:"5.6.3-9-3",title:"V1"},{mdcCode:"2:4",scpEcgCode:"5.6.3-9-4",title:"V2"},{mdcCode:"2:5",scpEcgCode:"5.6.3-9-5",title:"V3"},{mdcCode:"2:6",scpEcgCode:"5.6.3-9-6",title:"V4"},{mdcCode:"2:7",scpEcgCode:"5.6.3-9-7",title:"V5"},{mdcCode:"2:8",scpEcgCode:"5.6.3-9-8",title:"V6"}];if("MDC"===r){const r=e.find((e=>e.mdcCode===n));void 0!==r&&(t=r.title)}else if("SCPECG"===r){const r=e.find((e=>e.scpEcgCode===n));void 0!==r&&(t=r.title)}}t=t?t.replace(/[^a-zA-Z0-9_ ]/g,""):"",s.push(t.replace(/lead/i,"").replace(/\(?einthoven\)?/i,"").trim())}))}})),e.leads=[];const c=new Uint8Array(e.waveformData.find((e=>e)));let u;u=8===e.bitsAllocated?"UB"===e.sampleInterpretation?new Uint8Array(c.buffer,c.byteOffset,c.byteLength):new Int8Array(c.buffer,c.byteOffset,c.byteLength):new Int16Array(new Uint16Array(c.buffer,c.byteOffset,c.byteLength/Uint16Array.BYTES_PER_ELEMENT));let f=u.reduce(((e,t,n)=>(n%r==0?e.push([t]):e[e.length-1].push(t))&&e),[]);f=f[0].map(((e,t)=>f.map((e=>e[t]))));for(let e=0;e<r;e++)for(let t=0;t<f[e].length;t++)f[e][t]=(f[e][t]+a[e])*i[e];if(!0===t.applyLowPassFilter){const t=40;for(let n=0;n<r;n++)this._lowPassFilter(f[n],t,e.samplingFrequency)}if(l.length===r){const e={uV:1e3,mV:1};for(let t=0;t<r;t++)for(let n=0;n<f[t].length;n++)f[t][n]=f[t][n]/e[l[t]]}else if(0===l.length)for(let e=0;e<r;e++)for(let t=0;t<f[e].length;t++)f[e][t]=f[e][t]/1e3;for(let t=0;t<r;t++)e.leads.push({min:Math.min(...f[t]),max:Math.max(...f[t]),signal:f[t],source:s[t]});e.min=Math.min(...e.leads.map((e=>e.min))),e.max=Math.max(...e.leads.map((e=>e.max))),e.minMax=Math.max(Math.abs(Math.min(...e.leads.map((e=>e.min)))),Math.abs(Math.max(...e.leads.map((e=>e.max)))))}_sortLeads(e){const t=["I","II","III","aVR","aVL","aVF","V1","V2","V3","V4","V5","V6"];e.leads.sort(((e,n)=>{const r=t.indexOf(e.source),o=t.indexOf(n.source);return(r>-1?r:1/0)-(o>-1?o:1/0)}))}_lowPassFilter(e,t,n){const r=1/n,o=r/(1/(2*t*Math.PI)+r);let i=e[0];for(let t=0;t<e.length;t++)i+=o*(e[t]-i),e[t]=i}_extractInformation(e){const t=this.getElement("WaveformAnnotationSequence");if(void 0===t||!Array.isArray(t)||0===t.length)return[];const n=[];t.forEach((e=>{const t=e.ConceptNameCodeSequence;void 0!==t&&Array.isArray(t)&&0!==t.length&&t.forEach((t=>{const r=[{key:"QT Interval",unit:"ms"},{key:"QTc Interval",unit:"ms"},{key:"RR Interval",unit:"ms"},{key:"VRate",unit:"BPM"},{key:"QRS Duration",unit:"ms"},{key:"QRS Axis",unit:"°"},{key:"T Axis",unit:"°"},{key:"P Axis",unit:"°"},{key:"PR Interval",unit:"ms"}].find((e=>e.key===t.CodeMeaning));void 0!==e.NumericValue&&void 0!==r&&n.push({key:t.CodeMeaning,value:e.NumericValue,unit:r.unit})}))}));const r=n.find((e=>"RR Interval"===e.key));return!n.find((e=>"VRate"===e.key))&&r&&n.push({key:"VRate",value:Math.trunc(60/e.duration*e.samples/r.value),unit:"BPM"}),n}_extractAnnotation(){const e=this.getElement("WaveformAnnotationSequence");if(void 0===e||!Array.isArray(e)||0===e.length)return[];const t=[];return e.forEach((e=>{void 0!==e.UnformattedTextValue&&t.push(e.UnformattedTextValue)})),t}_renderLeadGrid(e,t,n,r){e.rect(0,t*r,n,r,p.DefaultGridBackgroundColor,p.DefaultGridForegroundColor,2);const o=[],i=[];for(let e=0;e<Math.trunc(r/d);e++)e%5==0?o.push({x1:0,y1:t*r+e*d,x2:n,y2:t*r+e*d}):i.push({x1:0,y1:t*r+e*d,x2:n,y2:t*r+e*d});e.path(o,p.DefaultGridForegroundColor,.75),e.path(i,p.DefaultGridForegroundColor,.25),o.length=0,i.length=0;for(let e=0;e<Math.trunc(n/d);e++)e%5==0?o.push({x1:e*d,y1:t*r,x2:e*d,y2:t*r+r}):i.push({x1:e*d,y1:t*r,x2:e*d,y2:t*r+r});e.path(o,p.DefaultGridForegroundColor,.75),e.path(i,p.DefaultGridForegroundColor,.25)}_renderLeadSignal(e,t,n,r,o,i){const a=o/t.samples,l=i/2;let s=0,c=r*i+(l-this._translate(n.signal[0],-Math.ceil(t.minMax),Math.ceil(t.minMax),-l,l));const u=[];for(let e=1;e<t.samples;e++){const o=a*e,f=r*i+(l-this._translate(n.signal[e],-Math.ceil(t.minMax),Math.ceil(t.minMax),-l,l));u.push({x1:s,y1:c,x2:o,y2:f}),s=o,c=f}e.path(u,p.DefaultSignalColor,1.25)}_translate(e,t,n,r,o){return r+(e-t)/(n-t)*(o-r)}_renderLeadTitle(e,t,n,r){e.text(2,n*r+10,t.source,p.DefaultTextColor,10,"bold")}}},880(e){e.exports=class{constructor(e,t,n){if(!e)throw new Error("Width should be provided");if(!t)throw new Error("Height should be provided");n=n||{r:255,g:255,b:255,a:255};const r=this._rgbaToRgbHexAndOpacity(n);this.svg=['<?xml version="1.0" encoding="UTF-8" standalone="no"?>','<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',`<svg viewBox="0 0 ${this._fixDecimal(e)} ${this._fixDecimal(t)}" xmlns="http://www.w3.org/2000/svg">`,`<rect width="100%" height="100%" fill="${r.color}" fill-opacity="${r.opacity}"/>`]}line(e,t,n,r,o,i){o=o||{r:0,g:0,b:0,a:255},i=i||1;const a=this._rgbaToRgbHexAndOpacity(o);this.svg.push(`<line x1="${this._fixDecimal(e)}" y1="${this._fixDecimal(t)}" x2="${this._fixDecimal(n)}" y2="${this._fixDecimal(r)}" stroke="${a.color}" stroke-opacity="${a.opacity}" stroke-width="${i}"/>`)}path(e,t,n,r,o){t=t||{r:0,g:0,b:0,a:255},n=n||1,r=r||"round",o=o||"round";const i=this._rgbaToRgbHexAndOpacity(t),a=Array.isArray(e)?e:[e],l=[];let s,c;a.forEach((e=>{e.x1!==s&&e.y1!==c?(l.push(`M ${this._fixDecimal(e.x1)} ${this._fixDecimal(e.y1)} L ${this._fixDecimal(e.x2)} ${this._fixDecimal(e.y2)}`),s=e.x2,c=e.y2):l.push(`L ${this._fixDecimal(e.x2)} ${this._fixDecimal(e.y2)}`)})),this.svg.push(`<path d="${l.join(" ")}" stroke="${i.color}" stroke-opacity="${i.opacity}" stroke-width="${n}" stroke-linejoin="${r}" stroke-linecap="${o}" fill="none" fill-opacity="none"/>`)}rect(e,t,n,r,o,i,a){o=o||{r:0,g:0,b:0,a:255},i=i||{r:0,g:0,b:0,a:255},a=a||1;const l=this._rgbaToRgbHexAndOpacity(o),s=this._rgbaToRgbHexAndOpacity(i);this.svg.push(`<rect x="${this._fixDecimal(e)}" y="${this._fixDecimal(t)}" width="${this._fixDecimal(n)}" height="${this._fixDecimal(r)}" fill="${l.color}" fill-opacity="${l.opacity}" stroke="${s.color}" stroke-opacity="${s.opacity}" stroke-width="${a}"/>`)}text(e,t,n,r,o,i){r=r||{r:0,g:0,b:0,a:255},o=o||1,i=i||"normal";const a=this._rgbaToRgbHexAndOpacity(r);this.svg.push(`<text x="${this._fixDecimal(e)}" y="${this._fixDecimal(t)}" font-size="${o}" font-weight="${i}" fill="${a.color}" fill-opacity="${a.opacity}">${n}</text>`)}toXmlString(){return this.svg.push("</svg>"),this.svg.join("\n")}_fixDecimal(e,t){return t=t||2,Number.isInteger(e)?e:e.toFixed(t)}_rgbaToRgbHexAndOpacity(e){return{color:"#"+(1<<24|e.r<<16|e.g<<8|e.b).toString(16).slice(1),opacity:this._fixDecimal(e.a/255,1)}}}},237(e,t,n){const r={DicomEcg:n(688),log:n(547),version:n(837)};e.exports=r},547(e,t,n){const r=n(213),o=n(935);o.reg(r),r.enableAll(!1),o.apply(r,{format:(e,t,n)=>`${n} -- ${e.toUpperCase()} --`,timestampFormatter:e=>e.toISOString()}),e.exports=r},837(e){e.exports="0.0.15"},111(t){"use strict";t.exports=e}},n={},function e(r){var o=n[r];if(void 0!==o)return o.exports;var i=n[r]={exports:{}};return t[r].call(i.exports,i,i.exports,e),i.exports}(237);var t,n}));