From bcffbb47698bb13c96306ece6a57628a099225e6 Mon Sep 17 00:00:00 2001 From: Jean Fernandez <55406257+jean-the-coder@users.noreply.github.com> Date: Fri, 15 Mar 2024 19:00:43 -0400 Subject: [PATCH] Parse reference ranges that use `<` and `>` (#450) * Add vscode config * Comment out empty tests causing issues running tests in vscode * Add Fishery, update test dependencies, update chart.js * Add factory for building fhir r4 observation object * Fix deprecation warnings in _mixins.scss * Update observation model for better value and reference range parsing * Add observation-bar-chart.component to pull out bar chart logic into reusable component * Use new component in observation resource and report lab component --- .gitignore | 8 + .vscode/launch.json | 26 +++ .vscode/settings.json | 6 + frontend/karma.conf.js | 4 +- frontend/package.json | 17 +- .../observation-bar-chart.component.html | 9 + .../observation-bar-chart.component.scss | 0 .../observation-bar-chart.component.spec.ts | 22 +++ .../observation-bar-chart.component.ts | 161 ++++++++++++++++ .../observation-bar-chart.stories.ts | 64 +++++++ .../components/fhir-card/fhir-card.module.ts | 3 + .../observation/observation.component.html | 8 +- .../observation/observation.component.ts | 115 ++--------- .../observation/observation.stories.ts | 53 ++---- .../report-labs-observation.component.html | 14 +- .../report-labs-observation.component.spec.ts | 9 +- .../report-labs-observation.component.ts | 178 +----------------- .../report-labs-observation.stories.ts | 5 +- .../dashboard-widget.component.spec.ts | 4 +- frontend/src/assets/scss/custom/_mixins.scss | 8 +- .../r4/resources/observation-r4-factory.ts | 152 +++++++++++++++ .../resources/observation-model.spec.ts | 88 +++++++++ .../lib/models/resources/observation-model.ts | 115 +++++++++-- frontend/yarn.lock | 152 ++++++++------- 24 files changed, 785 insertions(+), 436 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.html create mode 100644 frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.scss create mode 100644 frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.spec.ts create mode 100644 frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.ts create mode 100644 frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.stories.ts create mode 100644 frontend/src/lib/fixtures/factories/r4/resources/observation-r4-factory.ts diff --git a/.gitignore b/.gitignore index fd950fd4..565e020c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,13 @@ cmake-build-release/ # IntelliJ out/ +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + # mpeltonen/sbt-idea plugin .idea_modules/ @@ -70,3 +77,4 @@ fasten.db-shm fasten.db-wal backend/resources/related_versions.json +frontend/documentation.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..36b549e1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Go Tests", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/backend", + "args": [ + "-test.run" + ] + }, + { + "type": "chrome", + "request": "attach", + "name": "Attach Karma Chrome", + "address": "localhost", + "port": 9333, + "pathMapping": { + "/": "${workspaceRoot}/frontend", + "/base/": "${workspaceRoot}/frontend" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..bc09be7f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "karmaTestExplorer.karmaConfFilePath": "frontend/karma.conf.js", + "karmaTestExplorer.projectWorkspaces": [ + "frontend" + ] +} diff --git a/frontend/karma.conf.js b/frontend/karma.conf.js index 99a445ac..733dd485 100644 --- a/frontend/karma.conf.js +++ b/frontend/karma.conf.js @@ -52,8 +52,8 @@ module.exports = function(config) { customLaunchers: { ChromeHeadlessCI: { base: 'ChromeHeadless', - flags: ['--no-sandbox', '--disable-gpu'] - } + flags: ['--no-sandbox', '--disable-gpu', '--remote-debugging-port=9333'] + }, }, }); }; diff --git a/frontend/package.json b/frontend/package.json index 7e65e596..bc89532f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,7 +39,7 @@ "@webcomponents/webcomponentsjs": "^2.8.0", "asmcrypto.js": "^2.3.2", "bootstrap": "^4.4.1", - "chart.js": "^4.0.1", + "chart.js": "^4.4.2", "dwv": "^0.31.0", "fhirpath": "^3.3.0", "gridstack": "8.1.1", @@ -77,14 +77,15 @@ "@types/jasminewd2": "~2.0.3", "chromatic": "^6.19.8", "codelyzer": "^5.1.2", - "jasmine-core": "~3.5.0", - "jasmine-spec-reporter": "~5.0.0", - "karma": "~6.4.0", - "karma-chrome-launcher": "~3.1.0", - "karma-coverage": "^2.2.0", + "fishery": "^2.2.2", + "jasmine-core": "~5.1.2", + "jasmine-spec-reporter": "~7.0.0", + "karma": "~6.4.3", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "^2.2.1", "karma-coverage-istanbul-reporter": "~3.0.2", - "karma-jasmine": "~4.0.0", - "karma-jasmine-html-reporter": "^1.5.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "^2.1.0", "protractor": "~7.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.html b/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.html new file mode 100644 index 00000000..9a43e5e2 --- /dev/null +++ b/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.html @@ -0,0 +1,9 @@ + +
+ +
diff --git a/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.scss b/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.spec.ts b/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.spec.ts new file mode 100644 index 00000000..e3ea2278 --- /dev/null +++ b/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ObservationBarChartComponent } from './observation-bar-chart.component'; + +describe('ObservationBarChartComponent', () => { + let component: ObservationBarChartComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ ObservationBarChartComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ObservationBarChartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.ts b/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.ts new file mode 100644 index 00000000..ab005b4b --- /dev/null +++ b/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component.ts @@ -0,0 +1,161 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { formatDate } from '@angular/common'; +import { ObservationModel } from '../../../../../lib/models/resources/observation-model'; +import { ChartConfiguration } from 'chart.js'; +import { NgChartsModule } from 'ng2-charts'; + +const defaultChartHeight = 65; +const defaultChartEntryHeight = 30; + +@Component({ + standalone: true, + selector: 'observation-bar-chart', + imports: [ NgChartsModule ], + templateUrl: './observation-bar-chart.component.html', + styleUrls: ['./observation-bar-chart.component.scss'] +}) +export class ObservationBarChartComponent implements OnInit { + @Input() observations: [ObservationModel] + + chartHeight = defaultChartEntryHeight; + + // based on https://stackoverflow.com/questions/38889716/chartjs-2-stacked-bar-with-marker-on-top + // https://stackoverflow.com/questions/62711919/chart-js-horizontal-lines-per-bar + barChartData =[ + { + label: "Reference", + data: [], + dataLabels: [], + backgroundColor: "rgba(91, 71, 251,0.6)", + hoverBackgroundColor: "rgba(91, 71, 251,0.2)", + parsing: { + xAxisKey: 'range' + }, + tooltip: { + callbacks: { + label: function(context) { + return `${context.dataset.label}: ${context.dataset.dataLabels[context.dataIndex]}`; + } + } + } + }, + { + label: "Result", + data: [], + // @ts-ignore + dataLabels: [], + borderColor: "rgba(0,0,0,1)", + backgroundColor: "rgba(0,0,0,1)", + hoverBackgroundColor: "rgba(0,0,0,1)", + minBarLength: 3, + barPercentage: 1, + parsing: { + xAxisKey: 'value' + }, + // @ts-ignore + tooltip: { + callbacks: { + label: function(context) { + let label = `${context.dataset.label}: ${context.parsed.x}`; + + if (context.dataset.dataLabels[context.dataIndex]) { + return `${label} ${context.dataset.dataLabels[context.dataIndex]}`; + } + return label; + } + } + } + } + ] as ChartConfiguration<'bar'>['data']['datasets'] + + barChartLabels = [] // ["2020", "2018"] //["1","2","3","4","5","6","7","8"] + + barChartOptions = { + indexAxis: 'y', + maintainAspectRatio: false, + legend:{ + display: false, + }, + autoPadding: true, + //add padding to fix tooltip cutoff + layout: { + padding: { + left: 0, + right: 4, + top: 0, + bottom: 10 + } + }, + scales: { + y: { + stacked: true, + ticks: { + beginAtZero: true, + fontSize: 10, + min: 0, + }, + }, + x: { + scaleLabel:{ + display: false, + padding: 4, + }, + ticks: { + beginAtZero: true, + fontSize: 10, + min: 0, + }, + }, + } + } as ChartConfiguration<'bar'>['options'] + + barChartColors = [ + { + backgroundColor: 'white' + } + ]; + + constructor() { } + + ngOnInit(): void { + if(!this.observations || !this.observations[0]) { + return; + } + + let currentValues: number[] = [] + let referenceRanges = [] + + for(let observation of this.observations) { + let refRange = observation.reference_range; + + referenceRanges.push([refRange.low || 0, refRange.high || 0]); + currentValues.push(observation.value_quantity_value); + + if (observation.effective_date) { + this.barChartLabels.push(formatDate(observation.effective_date, "mediumDate", "en-US", undefined)); + } else { + this.barChartLabels.push('Unknown date'); + } + + this.barChartData[0]['dataLabels'].push(observation.referenceRangeDisplay()); + this.barChartData[1]['dataLabels'].push(observation.value_quantity_unit); + } + + let xAxisMax = Math.max(...currentValues) * 1.3; + this.barChartOptions.scales['x']['max'] = xAxisMax + + let updatedRefRanges = referenceRanges.map(range => { + if (range[0] && !range[1]) { + return [range[0], xAxisMax] + } else { + return [range[0], range[1]] + } + }); + + // @ts-ignore + this.barChartData[0].data = updatedRefRanges + this.barChartData[1].data = currentValues.map(v => [v, v]) + + this.chartHeight = defaultChartHeight + (defaultChartEntryHeight * currentValues.length) + } +} diff --git a/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.stories.ts b/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.stories.ts new file mode 100644 index 00000000..ccea23e5 --- /dev/null +++ b/frontend/src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.stories.ts @@ -0,0 +1,64 @@ +import type { Meta, StoryObj } from '@storybook/angular'; +import { fhirVersions } from "../../../../../lib/models/constants"; +import { ObservationBarChartComponent } from './observation-bar-chart.component'; +import { ObservationModel } from 'src/lib/models/resources/observation-model'; +import { observationR4Factory } from 'src/lib/fixtures/factories/r4/resources/observation-r4-factory'; + +// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction +const meta: Meta = { + title: 'Fhir Card/Common/ObservationBarChart', + component: ObservationBarChartComponent, + decorators: [ + ], + tags: ['autodocs'], + render: (args: ObservationBarChartComponent) => ({ + props: { + backgroundColor: null, + ...args, + }, + }), + argTypes: { + observations: { + control: 'object', + } + }, +}; + +export default meta; +type Story = StoryObj; + +export const NoRange: Story = { + args: { + observations: [new ObservationModel(observationR4Factory.build(), fhirVersions.R4)] + } +}; + +export const Range: Story = { + args: { + observations: [new ObservationModel(observationR4Factory.referenceRange().build(), fhirVersions.R4)] + } +}; + +export const RangeOnlyLow: Story = { + args: { + observations: [new ObservationModel(observationR4Factory.referenceRangeOnlyLow().build(), fhirVersions.R4)] + } +}; + +export const RangeOnlyLowText: Story = { + args: { + observations: [new ObservationModel(observationR4Factory.referenceRangeStringOnlyLow().build(), fhirVersions.R4)] + } +}; + +export const RangeOnlyHigh: Story = { + args: { + observations: [new ObservationModel(observationR4Factory.referenceRangeOnlyHigh().build(), fhirVersions.R4)] + } +}; + +export const RangeOnlyHighText: Story = { + args: { + observations: [new ObservationModel(observationR4Factory.referenceRangeStringOnlyHigh().build(), fhirVersions.R4)] + } +}; diff --git a/frontend/src/app/components/fhir-card/fhir-card.module.ts b/frontend/src/app/components/fhir-card/fhir-card.module.ts index 60bbfece..67be9407 100644 --- a/frontend/src/app/components/fhir-card/fhir-card.module.ts +++ b/frontend/src/app/components/fhir-card/fhir-card.module.ts @@ -28,6 +28,7 @@ import {FhirCardComponent} from './fhir-card/fhir-card.component'; import {FhirCardOutletDirective} from './fhir-card/fhir-card-outlet.directive'; import { EncounterComponent } from './resources/encounter/encounter.component'; import { RtfComponent } from './datatypes/rtf/rtf.component'; +import { ObservationBarChartComponent } from './common/observation-bar-chart/observation-bar-chart.component'; @@ -36,6 +37,7 @@ import { RtfComponent } from './datatypes/rtf/rtf.component'; //common CommonModule, BadgeComponent, + ObservationBarChartComponent, //datatypes TableComponent, BinaryTextComponent, @@ -75,6 +77,7 @@ import { RtfComponent } from './datatypes/rtf/rtf.component'; //common BadgeComponent, TableComponent, + ObservationBarChartComponent, //datatypes BinaryTextComponent, CodableConceptComponent, diff --git a/frontend/src/app/components/fhir-card/resources/observation/observation.component.html b/frontend/src/app/components/fhir-card/resources/observation/observation.component.html index 48f33fe3..5922653a 100644 --- a/frontend/src/app/components/fhir-card/resources/observation/observation.component.html +++ b/frontend/src/app/components/fhir-card/resources/observation/observation.component.html @@ -14,13 +14,7 @@

Observations are a central element in healthcare, used to support diagnosis, monitor progress, determine baselines and patterns and even capture demographic characteristics.

- + @@ -53,15 +54,12 @@
- +
- - - diff --git a/frontend/src/app/components/report-labs-observation/report-labs-observation.component.spec.ts b/frontend/src/app/components/report-labs-observation/report-labs-observation.component.spec.ts index b92fd29d..d5aa5c1d 100644 --- a/frontend/src/app/components/report-labs-observation/report-labs-observation.component.spec.ts +++ b/frontend/src/app/components/report-labs-observation/report-labs-observation.component.spec.ts @@ -1,8 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { ReportLabsObservationComponent } from './report-labs-observation.component'; -import {PipesModule} from '../../pipes/pipes.module'; -import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap'; +import { PipesModule } from '../../pipes/pipes.module'; +import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'; describe('ReportLabsObservationComponent', () => { let component: ReportLabsObservationComponent; @@ -10,8 +9,8 @@ describe('ReportLabsObservationComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PipesModule, NgbCollapseModule], - declarations: [ ReportLabsObservationComponent ] + imports: [PipesModule], + declarations: [ ReportLabsObservationComponent, NgbCollapse ], }) .compileComponents(); diff --git a/frontend/src/app/components/report-labs-observation/report-labs-observation.component.ts b/frontend/src/app/components/report-labs-observation/report-labs-observation.component.ts index 3852ba5c..4bd242ee 100644 --- a/frontend/src/app/components/report-labs-observation/report-labs-observation.component.ts +++ b/frontend/src/app/components/report-labs-observation/report-labs-observation.component.ts @@ -1,11 +1,6 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {ResourceFhir} from '../../models/fasten/resource_fhir'; -import {ChartConfiguration} from 'chart.js'; -// import { ChartConfiguration, ChartData, ChartEvent, ChartType } from 'chart.js'; -// import { BaseChartDirective } from 'ng2-charts'; -import * as fhirpath from 'fhirpath'; -import {formatDate} from '@angular/common'; - +import { Component, Input, OnInit } from '@angular/core'; +import { ResourceFhir } from '../../models/fasten/resource_fhir'; +import { ObservationModel } from 'src/lib/models/resources/observation-model'; @Component({ selector: 'app-report-labs-observation', @@ -13,186 +8,23 @@ import {formatDate} from '@angular/common'; styleUrls: ['./report-labs-observation.component.scss'] }) export class ReportLabsObservationComponent implements OnInit { - @Input() observations: ResourceFhir[] = [] @Input() observationCode: string @Input() observationTitle: string + observationModels: ObservationModel[] = [] firstObservation: ResourceFhir = null - // based on https://stackoverflow.com/questions/38889716/chartjs-2-stacked-bar-with-marker-on-top - // https://stackoverflow.com/questions/62711919/chart-js-horizontal-lines-per-bar - - - chartHeight = 60 - - barChartData =[ - // { - // label: "Current", - // backgroundColor: 'rgba(255, 0, 128, 1)', - // data: [], - // xAxisID: "x-axis-current" - // }, - // { - // label: "Reference", - // backgroundColor: 'rgba(99,189,50,0.2)', - // data: [], - // xAxisID: "x-axis-ref" - // }, - - - - { - label: "Reference", - data: [[55,102], [44,120]], - backgroundColor: "rgba(91, 71, 251,0.6)", - hoverBackgroundColor: "rgba(91, 71, 251,0.2)" - },{ - label: "Current", - data: [[80,81], [130,131]], - borderColor: "rgba(0,0,0,1)", - backgroundColor: "rgba(0,0,0,1)", - hoverBackgroundColor: "rgba(0,0,0,1)", - minBarLength: 3, - barPercentage: 1, - tooltip: { - - } - // id: "x-axis-current", - //important settings - - //set the width of the line ( or point ) - // pointRadius: 50, - // don´t show line betrween points - // showLine: false, - //change points of line chart to line style ( little bit confusin why it´s named point anyway ) - // pointStyle: 'line', - - //chart type - // type: "line", - } - ] as ChartConfiguration<'bar'>['data']['datasets'] - - barChartLabels = [] // ["2020", "2018"] //["1","2","3","4","5","6","7","8"] - - barChartOptions = { - indexAxis: 'y', - legend:{ - display: false, - }, - //add padding to fix tooltip cutoff - layout: { - padding: { - left: 0, - right: 4, - top: 0, - bottom: 10 - } - }, - scales: { - y: { - stacked: true, - ticks: { - beginAtZero: true, - fontSize: 10, - min: 0, - // max: 80 - }, - }, - x: { - scaleLabel:{ - display: false, - labelString: "xaxis", - padding: 4, - }, - // stacked: true, - ticks: { - beginAtZero: true, - fontSize: 10, - min: 0, - // max: 80 - }, - - }, - } - } as ChartConfiguration<'bar'>['options'] - - barChartColors = [ - { - backgroundColor: 'white' - } - ]; - constructor() { } ngOnInit(): void { - - let currentValues: number[] = [] - - let referenceRanges = [] - //sort observations this.observations = this.observations?.sort((a, b) => a.sort_date > b.sort_date ? -1 : a.sort_date < b.sort_date ? 1 : 0) if(this.observations.length > 0){ this.firstObservation = this.observations[0] } - for(let observation of this.observations){ - //get label - this.barChartLabels.push( - formatDate(fhirpath.evaluate(observation.resource_raw, "Observation.effectiveDateTime")[0], "mediumDate", "en-US", undefined) - ) - //get current value - // let currentValue = fhirpath.evaluate(observation.resource_raw, "Observation.valueQuantity.value")[0] - // if(currentValue != null){ - // currentValues.push([currentValue, currentValue]) - // } else { - // currentValues.push([]) - // } - currentValues.push(fhirpath.evaluate(observation.resource_raw, "Observation.valueQuantity.value")[0]) - - //set chart x-axis label - let units = fhirpath.evaluate(observation.resource_raw, "Observation.valueQuantity.unit")[0] - - //TODO: fix x-axis label - // if(units){ - // - // (this.barChartOptions as ChartConfiguration<'bar'>['options']).scales['x']['scaleLabel'].display = true - // (this.barChartOptions as ChartConfiguration<'bar'>['options']).scales['y']['scaleLabel'].labelString = units - // } - - - //add low/high ref value blocks - // let referenceLow = fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.low.value")[0] - // let referenceHigh = fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.high.value")[0] - // if (referenceLow != null && referenceHigh != null){ - // referenceRanges.push([referenceLow, referenceHigh]) - // } else { - // referenceRanges.push([0,0]) - // } - referenceRanges.push([ - fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.low.value")[0], - fhirpath.evaluate(observation.resource_raw, "Observation.referenceRange.high.value")[0] - ]) - } - - - - // @ts-ignore - this.barChartData[0].data = referenceRanges - this.barChartData[1].data = currentValues.map(v => [v, v]) - // this.barChartData[1].data = currentValues - - let suggestedMax = Math.max(...currentValues) * 1.1; - this.barChartOptions.scales['x']['suggestedMax'] = suggestedMax - - console.log(this.observationTitle, this.barChartData[0].data, this.barChartData[1].data) - - - if(currentValues.length > 1){ - this.chartHeight = 30 * currentValues.length - } + this.observationModels = this.observations.map(ob => new ObservationModel(ob.resource_raw)) } - } diff --git a/frontend/src/app/components/report-labs-observation/report-labs-observation.stories.ts b/frontend/src/app/components/report-labs-observation/report-labs-observation.stories.ts index 7e21a1e6..258545b6 100644 --- a/frontend/src/app/components/report-labs-observation/report-labs-observation.stories.ts +++ b/frontend/src/app/components/report-labs-observation/report-labs-observation.stories.ts @@ -14,6 +14,7 @@ import { Observable, of } from 'rxjs'; import R4Example1Json from "../../../lib/fixtures/r4/resources/observation/example1.json"; import { Html as GlossaryLookupHtml } from '../glossary-lookup/glossary-lookup.stories'; +import { ObservationBarChartComponent } from '../fhir-card/common/observation-bar-chart/observation-bar-chart.component'; const withHttpClientProvider: DecoratorFunction = (storyFunc, context) => { @@ -39,8 +40,8 @@ const meta: Meta = { decorators: [ withHttpClientProvider, moduleMetadata({ - imports: [PipesModule, GlossaryLookupComponent, NgChartsModule, RouterTestingModule, HttpClientModule], - declarations: [NgbCollapse], + imports: [PipesModule, GlossaryLookupComponent, NgChartsModule, RouterTestingModule, HttpClientModule, ObservationBarChartComponent], + declarations: [ NgbCollapse ], providers: [], }) ], diff --git a/frontend/src/app/widgets/dashboard-widget/dashboard-widget.component.spec.ts b/frontend/src/app/widgets/dashboard-widget/dashboard-widget.component.spec.ts index b7928fdc..77d13e51 100644 --- a/frontend/src/app/widgets/dashboard-widget/dashboard-widget.component.spec.ts +++ b/frontend/src/app/widgets/dashboard-widget/dashboard-widget.component.spec.ts @@ -248,8 +248,8 @@ describe('DashboardWidgetComponent', () => { }); }) - describe('Vitals - ListWidget', () => {}) - describe('Resource Aggregation - DonutWidget', () => {}) + // describe('Vitals - ListWidget', () => {}) + // describe('Resource Aggregation - DonutWidget', () => {}) }) diff --git a/frontend/src/assets/scss/custom/_mixins.scss b/frontend/src/assets/scss/custom/_mixins.scss index 679bd42a..596cf1bb 100755 --- a/frontend/src/assets/scss/custom/_mixins.scss +++ b/frontend/src/assets/scss/custom/_mixins.scss @@ -45,7 +45,7 @@ $color2: $color; $base2: $base; - $deg: ($perc/100*360)+deg; + $deg: calc($perc / 100 * 360) + deg; $deg1: 90deg; $deg2: $deg; @@ -54,7 +54,7 @@ $base: $color; $color: $base2; $color2: $base2; - $deg1: ($perc/100*360+90)+deg; + $deg1: calc($perc / 100 * 360 + 90) + deg; $deg2: 0deg; } @@ -66,14 +66,14 @@ .slice { &.one { - clip: rect(0 $size $size/2 0); + clip: rect(0 $size calc($size / 2) 0); -webkit-transform: rotate($deg1); transform: rotate($deg1); background: $color; } &.two { - clip: rect(0 $size/2 $size 0); + clip: rect(0 calc($size / 2) $size 0); -webkit-transform: rotate($deg2); transform: rotate($deg2); background: $color2; diff --git a/frontend/src/lib/fixtures/factories/r4/resources/observation-r4-factory.ts b/frontend/src/lib/fixtures/factories/r4/resources/observation-r4-factory.ts new file mode 100644 index 00000000..5c85d375 --- /dev/null +++ b/frontend/src/lib/fixtures/factories/r4/resources/observation-r4-factory.ts @@ -0,0 +1,152 @@ +import { Factory } from 'fishery'; + +class ObservationR4Factory extends Factory<{}> { + + valueString(value?: string) { + return this.params({ + valueQuantity: null, + valueString: value || '5.5mmol/l' + }) + } + + referenceRange(high?: number, low?: number) { + return this.params({ + referenceRange: [ + { + low: { + value: low || 3.1, + unit: 'mmol/l', + system: 'http://unitsofmeasure.org', + code: 'mmol/L' + }, + high: { + value: high || 6.5, + unit: 'mmol/l', + system: 'http://unitsofmeasure.org', + code: 'mmol/L' + } + } + ] + }) + } + + referenceRangeOnlyHigh(value?: number) { + return this.params({ + referenceRange: [ + { + high: { + value: value || 6.5, + unit: 'mmol/l', + system: 'http://unitsofmeasure.org', + code: 'mmol/L' + } + } + ] + }); + }; + + referenceRangeOnlyLow(value?: number) { + return this.params({ + referenceRange: [ + { + low: { + value: value || 3.1, + unit: 'mmol/l', + system: 'http://unitsofmeasure.org', + code: 'mmol/L' + } + } + ] + }); + }; + + referenceRangeString(range?: string) { + return this.params({ + referenceRange: [ + { + text: range || '3.1mmol/l-6.3mmol/l' + } + ] + }); + }; + + referenceRangeStringOnlyHigh(high?: string) { + return this.params({ + referenceRange: [ + { + text: high || '<=5.5' + } + ] + }); + }; + + referenceRangeStringOnlyLow(low?: | string) { + return this.params({ + referenceRange: [ + { + text: low || '>=4.5' + } + ] + }); + }; +} + +export const observationR4Factory = ObservationR4Factory.define(() => ( + { + resourceType: 'Observation', + id: 'f001', + text: { + status: 'generated', + div: "

Generated Narrative with Details

id: example

status: final

category: Vital Signs (Details : {http://terminology.hl7.org/CodeSystem/observation-category code 'vital-signs' = 'Vital Signs', given as 'Vital Signs'})

code: Body Weight (Details : {LOINC code '29463-7' = 'Body weight', given as 'Body Weight'}; {LOINC code '3141-9' = 'Body weight Measured', given as 'Body weight Measured'}; {SNOMED CT code '27113001' = 'Body weight', given as 'Body weight'}; {http://acme.org/devices/clinical-codes code 'body-weight' = 'body-weight', given as 'Body Weight'})

subject: Patient/example

encounter: Encounter/example

effective: 28/03/2016

value: 185 lbs (Details: UCUM code [lb_av] = 'lb_av')

" + }, + identifier: [ + { + use: 'official', + system: 'http://www.bmc.nl/zorgportal/identifiers/observations', + value: '6323' + } + ], + status: 'final', + code: { + coding: [ + { + system: 'http://loinc.org', + code: '15074-8', + display: 'Glucose [Moles/volume] in Blood' + } + ] + }, + subject: { + reference: 'Patient/f001', + display: 'P. van de Heuvel' + }, + effectiveDateTime: '2016-03-28', + effectivePeriod: { + start: '2013-04-02T09:30:10+01:00' + }, + issued: '2013-04-03T15:30:10+01:00', + performer: [ + { + reference: 'Practitioner/f005', + display: 'A. Langeveld' + } + ], + valueQuantity: { + value: 6.3, + unit: 'mmol/l', + system: 'http://unitsofmeasure.org', + code: 'mmol/L' + }, + interpretation: [ + { + coding: [ + { + system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation', + code: 'H', + display: 'High' + } + ] + } + ], + } +)); diff --git a/frontend/src/lib/models/resources/observation-model.spec.ts b/frontend/src/lib/models/resources/observation-model.spec.ts index 7baec831..ace3bb7c 100644 --- a/frontend/src/lib/models/resources/observation-model.spec.ts +++ b/frontend/src/lib/models/resources/observation-model.spec.ts @@ -1,7 +1,95 @@ import { ObservationModel } from './observation-model'; +import { fhirVersions } from '../constants'; +import { observationR4Factory } from 'src/lib/fixtures/factories/r4/resources/observation-r4-factory'; describe('ObservationModel', () => { it('should create an instance', () => { expect(new ObservationModel({})).toBeTruthy(); }); + + describe('parsing value', () => { + it('reads from valueQuantity.value if set', () => { + let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4); + + expect(observation.value_quantity_value).toEqual(6.3); + }); + + it('parses valueString correctly when value is a number if valueQuantity.value not set', () => { + let observation = new ObservationModel(observationR4Factory.valueString().build(), fhirVersions.R4); + + expect(observation.value_quantity_value).toEqual(5.5); + }); + }); + + describe('parsing unit', () => { + it('reads from valueQuantity.unit if set', () => { + let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4); + + expect(observation.value_quantity_unit).toEqual('mmol/l'); + }); + + it('reads from valueString if valueQuantity.unit not set', () => { + let observation = new ObservationModel(observationR4Factory.valueString().build(), fhirVersions.R4); + + expect(observation.value_quantity_unit).toEqual('mmol/l'); + }); + }); + + describe('parsing reference range', () => { + it('parses referenceRange correctly when high and low are not set', () => { + let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4); + + expect(observation.reference_range).toEqual({ low: null, high: null }); + }); + + it('parses referenceRange correctly when high and low are set', () => { + let observation = new ObservationModel(observationR4Factory.referenceRange().build(), fhirVersions.R4); + + expect(observation.reference_range).toEqual({ low: 3.1, high: 6.5 }); + }); + + describe('when referenceRange.text is set', () => { + it('parses values correctly when there is a high and a low', () => { + let tests = [ + { text: '50.3-109.2', result: { low: 50.3, high: 109.2 } }, + { text: '50.3mg/L-109.2mg/L', result: { low: 50.3, high: 109.2 } }, + { text: '50.3-109.2mg/L', result: { low: 50.3, high: 109.2 } }, + { text: '50.3mg/L-109.2', result: { low: 50.3, high: 109.2 } } + ] + + for(let test of tests) { + let observation = new ObservationModel(observationR4Factory.referenceRangeString(test.text).build(), fhirVersions.R4); + expect(observation.reference_range).toEqual(test.result) + } + }); + + it('parses values correctly when there is only a low', () => { + let tests = [ + { text: '>50.3', result: { low: 50.3, high: null } }, + { text: '>50.3mg/L', result: { low: 50.3, high: null } }, + { text: '>=50.3', result: { low: 50.3, high: null } }, + { text: '>=50.3mg/L', result: { low: 50.3, high: null } } + ] + + for(let test of tests) { + let observation = new ObservationModel(observationR4Factory.referenceRangeStringOnlyLow(test.text).build(), fhirVersions.R4); + expect(observation.reference_range).toEqual(test.result) + } + }); + + it('parses values correctly when there is only a high', () => { + let tests = [ + { text: '<109.2', result: { low: null, high: 109.2 } }, + { text: '<109.2mg/L', result: { low: null, high: 109.2 } }, + { text: '<=109.2', result: { low: null, high: 109.2 } }, + { text: '<=109.2mg/L', result: { low: null, high: 109.2 } } + ] + + for(let test of tests) { + let observation = new ObservationModel(observationR4Factory.referenceRangeStringOnlyHigh(test.text).build(), fhirVersions.R4); + expect(observation.reference_range).toEqual(test.result) + } + }); + }); + }); }); diff --git a/frontend/src/lib/models/resources/observation-model.ts b/frontend/src/lib/models/resources/observation-model.ts index baa8d4ab..ca3b1d34 100644 --- a/frontend/src/lib/models/resources/observation-model.ts +++ b/frontend/src/lib/models/resources/observation-model.ts @@ -2,17 +2,20 @@ import {fhirVersions, ResourceType} from '../constants'; import * as _ from "lodash"; import {CodableConceptModel, hasValue} from '../datatypes/codable-concept-model'; import {ReferenceModel} from '../datatypes/reference-model'; -import {CodingModel} from '../datatypes/coding-model'; import {FastenDisplayModel} from '../fasten/fasten-display-model'; import {FastenOptions} from '../fasten/fasten-options'; +interface referenceRangeHash { + low: number | null, + high: number | null +} export class ObservationModel extends FastenDisplayModel { code: CodableConceptModel | undefined effective_date: string code_coding_display: string code_text: string - value_quantity_value: number | string + value_quantity_value: number value_quantity_unit: string status: string value_codeable_concept_text: string @@ -20,25 +23,19 @@ export class ObservationModel extends FastenDisplayModel { value_codeable_concept_coding: string value_quantity_value_number: number subject: ReferenceModel | undefined - reference_range: { - low: { - value: number - } - high: { - value: number - } - }[] | undefined + fhirResource: any + reference_range: referenceRangeHash constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) { super(fastenOptions) + this.fhirResource = fhirResource this.source_resource_type = ResourceType.Observation this.effective_date = _.get(fhirResource, 'effectiveDateTime'); this.code = _.get(fhirResource, 'code'); this.code_coding_display = _.get(fhirResource, 'code.coding.0.display'); this.code_text = _.get(fhirResource, 'code.text', ''); - this.value_quantity_value = _.get(fhirResource, 'valueQuantity.value', ''); - // const issued = _.get(fhirResource, 'issued', ''); - this.value_quantity_unit = _.get(fhirResource, 'valueQuantity.unit', ''); + this.value_quantity_value = this.parseValue(); + this.value_quantity_unit = this.parseUnit(); this.status = _.get(fhirResource, 'status', ''); this.value_codeable_concept_text = _.get( fhirResource, @@ -54,7 +51,97 @@ export class ObservationModel extends FastenDisplayModel { [], ); - this.reference_range = _.get(fhirResource, 'referenceRange', []); + this.reference_range = this.parseReferenceRange(); this.subject = _.get(fhirResource, 'subject'); } + + private parseValue(): number { + // TODO: parseFloat would return NaN if it can't parse. Need to check and make sure that doesn't cause issues + return this.valueQuantity() || parseFloat(this.valueString()) + } + + private parseUnit(): string { + return this.valueUnit() || this.valueStringUnit() + } + + // Look for the observation's numeric value. Use this first before valueString which is a backup if this can't be found. + private valueQuantity(): number { + // debugger + return _.get(this.fhirResource, "valueQuantity.value"); + } + + // Look for the observation's numeric value. Use this first before valueStringUnit which is a backup if this can't be found. + private valueUnit(): string { + return _.get(this.fhirResource, "valueQuantity.unit"); + } + + // Use if valueQuantity can't be found. This will check for valueString and attempt to parse the first number in the string + private valueString(): string { + return _.get(this.fhirResource, "valueString")?.match(/(?[\d.]*)(?.*)/).groups.value; + } + + // Use if valueUnit can't be found. + private valueStringUnit(): string { + return _.get(this.fhirResource, "valueString")?.match(/(?[\d.]*)(?.*)/).groups.text; + } + + private referenceRangeFromString(str: string): referenceRangeHash { + let matches = str?.match(/(?[\d.]*)?(?[^\d]*)?(?[\d.]*)?/) + + if(!matches) { + return { low: null, high: null } + } + + if (!!matches.groups['value1'] && !!matches.groups['value2']) { + return { + low: parseFloat(matches.groups['value1']), + high: parseFloat(matches.groups['value2']) + } + } + + if (['<', '<='].includes(matches.groups['operator'])) { + return { + low: null, + high: parseFloat(matches.groups['value2']) + } + } else { // > >= + return { + low: parseFloat(matches.groups['value2']), + high: null + } + } + } + + private parseReferenceRange(): referenceRangeHash { + let refRangeObject = _.get(this.fhirResource, "referenceRange.0") + + if (refRangeObject?.low || refRangeObject?.high) { + return { + low: refRangeObject.low?.value, + high: refRangeObject.high?.value + } + } + + return this.referenceRangeFromString(refRangeObject?.text) + } + + public referenceRangeDisplay(): string { + // If text was sent just show it since we aren't storing difference between <= and <. + // Likely doesn't really matter, but might as well if we have that data. + if (_.get(this.fhirResource, 'referenceRange.0.text')) { + return _.get(this.fhirResource, 'referenceRange.0.text'); + } + + let refRange = this.parseReferenceRange() + + if (refRange['low'] && refRange['high']) { + return `${refRange['low']}\u{2013}${refRange['high']}`; + } else if (refRange['low']) { + return `> ${refRange['low']}`; + } else if (refRange['high']) { + return `< ${refRange['high']}`; + } + + return ''; + } } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 760aa0da..9b1613e7 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4970,10 +4970,10 @@ chardet@^0.7.0: resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chart.js@^4.0.1: - version "4.3.0" - resolved "https://registry.npmjs.org/chart.js/-/chart.js-4.3.0.tgz#ac363030ab3fec572850d2d872956f32a46326a1" - integrity sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g== +chart.js@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.2.tgz#95962fa6430828ed325a480cc2d5f2b4e385ac31" + integrity sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg== dependencies: "@kurkle/color" "^0.3.0" @@ -5592,7 +5592,7 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -6052,15 +6052,15 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -engine.io-parser@~5.0.3: - version "5.0.6" - resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" - integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== +engine.io-parser@~5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" + integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== -engine.io@~6.2.1: - version "6.2.1" - resolved "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz#e3f7826ebc4140db9bbaa9021ad6b1efb175878f" - integrity sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA== +engine.io@~6.5.2: + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.4.tgz#6822debf324e781add2254e912f8568508850cdc" + integrity sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -6070,8 +6070,8 @@ engine.io@~6.2.1: cookie "~0.4.1" cors "~2.8.5" debug "~4.3.1" - engine.io-parser "~5.0.3" - ws "~8.2.3" + engine.io-parser "~5.2.1" + ws "~8.11.0" enhanced-resolve@^5.10.0, enhanced-resolve@^5.7.0: version "5.13.0" @@ -6840,6 +6840,13 @@ findit2@^2.2.3: resolved "https://registry.npmjs.org/findit2/-/findit2-2.2.3.tgz#58a466697df8a6205cdfdbf395536b8bd777a5f6" integrity sha512-lg/Moejf4qXovVutL0Lz4IsaPoNYMuxt4PA0nGqFxnJ1CTTGGlEO2wKgoDpwknhvZ8k4Q2F+eesgkLbG2Mxfog== +fishery@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/fishery/-/fishery-2.2.2.tgz#94d3d9380295dd3ce555021e9353c5348b8beb77" + integrity sha512-jeU0nDhPHJkupmjX+r9niKgVMTBDB8X+U/pktoGHAiWOSyNlMd0HhmqnjrpjUOCDPJYaSSu4Ze16h6dZOKSp2w== + dependencies: + lodash.mergewith "^4.6.2" + flatted@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" @@ -8138,25 +8145,25 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" -jasmine-core@^3.6.0: - version "3.99.1" - resolved "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz#5bfa4b2d76618868bfac4c8ff08bb26fffa4120d" - integrity sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg== +jasmine-core@^4.1.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-4.6.0.tgz#6884fc3d5b66bf293e422751eed6d6da217c38f5" + integrity sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ== jasmine-core@~2.8.0: version "2.8.0" resolved "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" integrity sha512-SNkOkS+/jMZvLhuSx1fjhcNWUC/KG6oVyFUGkSBEr9n1axSNduWU8GlI7suaHXr4yxjet6KjrUZxUTE5WzzWwQ== -jasmine-core@~3.5.0: - version "3.5.0" - resolved "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz#132c23e645af96d85c8bca13c8758b18429fc1e4" - integrity sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA== +jasmine-core@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-5.1.2.tgz#8f2789faa79ef1ffad7abab6bff8d4bd661094f7" + integrity sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA== -jasmine-spec-reporter@~5.0.0: - version "5.0.2" - resolved "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-5.0.2.tgz#b61288ab074ad440dc2477c4d42840b0e74a6b95" - integrity sha512-6gP1LbVgJ+d7PKksQBc2H0oDGNRQI3gKUsWlswKaQ2fif9X5gzhQcgM5+kiJGCQVurOG09jqNhk7payggyp5+g== +jasmine-spec-reporter@~7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz#94b939448e63d4e2bd01668142389f20f0a8ea49" + integrity sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg== dependencies: colors "1.4.0" @@ -8405,10 +8412,10 @@ jszip@~3.7.0: readable-stream "~2.3.6" set-immediate-shim "~1.0.1" -karma-chrome-launcher@~3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz#baca9cc071b1562a1db241827257bfe5cab597ea" - integrity sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ== +karma-chrome-launcher@~3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== dependencies: which "^1.2.1" @@ -8423,10 +8430,10 @@ karma-coverage-istanbul-reporter@~3.0.2: istanbul-reports "^3.0.2" minimatch "^3.0.4" -karma-coverage@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.0.tgz#64f838b66b71327802e7f6f6c39d569b7024e40c" - integrity sha512-gPVdoZBNDZ08UCzdMHHhEImKrw1+PAOQOIiffv1YsvxFhBjqvo/SVXNk4tqn1SYqX0BJZT6S/59zgxiBe+9OuA== +karma-coverage@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-2.2.1.tgz#e1cc074f93ace9dc4fb7e7aeca7135879c2e358c" + integrity sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A== dependencies: istanbul-lib-coverage "^3.2.0" istanbul-lib-instrument "^5.1.0" @@ -8435,17 +8442,17 @@ karma-coverage@^2.2.0: istanbul-reports "^3.0.5" minimatch "^3.0.4" -karma-jasmine-html-reporter@^1.5.0: - version "1.7.0" - resolved "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.7.0.tgz#52c489a74d760934a1089bfa5ea4a8fcb84cc28b" - integrity sha512-pzum1TL7j90DTE86eFt48/s12hqwQuiD+e5aXx2Dc9wDEn2LfGq6RoAxEZZjFiN0RDSCOnosEKRZWxbQ+iMpQQ== +karma-jasmine-html-reporter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz#f951ad00b08d61d03595402c914d1a589c4930e3" + integrity sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ== -karma-jasmine@~4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.2.tgz#386db2a3e1acc0af5265c711f673f78f1e4938de" - integrity sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g== +karma-jasmine@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-5.1.0.tgz#3af4558a6502fa16856a0f346ec2193d4b884b2f" + integrity sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ== dependencies: - jasmine-core "^3.6.0" + jasmine-core "^4.1.0" karma-source-map-support@1.4.0: version "1.4.0" @@ -8454,10 +8461,10 @@ karma-source-map-support@1.4.0: dependencies: source-map-support "^0.5.5" -karma@~6.4.0: - version "6.4.1" - resolved "https://registry.npmjs.org/karma/-/karma-6.4.1.tgz#f2253716dd3a41aaa813fa9f54b6ee047e1127d9" - integrity sha512-Cj57NKOskK7wtFWSlMvZf459iX+kpYIPXmkNUzP2WAFcA7nhr/ALn5R7sw3w+1udFDcpMx/tuB8d5amgm3ijaA== +karma@~6.4.3: + version "6.4.3" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.3.tgz#763e500f99597218bbb536de1a14acc4ceea7ce8" + integrity sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q== dependencies: "@colors/colors" "1.5.0" body-parser "^1.19.0" @@ -8478,7 +8485,7 @@ karma@~6.4.0: qjobs "^1.2.0" range-parser "^1.2.1" rimraf "^3.0.2" - socket.io "^4.4.1" + socket.io "^4.7.2" source-map "^0.6.1" tmp "^0.2.1" ua-parser-js "^0.7.30" @@ -8652,6 +8659,11 @@ lodash.get@~4.4.2: resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -11349,30 +11361,34 @@ smart-buffer@^4.2.0: resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -socket.io-adapter@~2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6" - integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg== +socket.io-adapter@~2.5.2: + version "2.5.4" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz#4fdb1358667f6d68f25343353bd99bd11ee41006" + integrity sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg== + dependencies: + debug "~4.3.4" + ws "~8.11.0" -socket.io-parser@~4.2.1: - version "4.2.2" - resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" - integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@^4.4.1: - version "4.5.4" - resolved "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz#a4513f06e87451c17013b8d13fdfaf8da5a86a90" - integrity sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ== +socket.io@^4.7.2: + version "4.7.4" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.4.tgz#2401a2d7101e4bdc64da80b140d5d8b6a8c7738b" + integrity sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw== dependencies: accepts "~1.3.4" base64id "~2.0.0" + cors "~2.8.5" debug "~4.3.2" - engine.io "~6.2.1" - socket.io-adapter "~2.4.0" - socket.io-parser "~4.2.1" + engine.io "~6.5.2" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" sockjs@^0.3.24: version "0.3.24" @@ -12778,10 +12794,10 @@ ws@^8.4.2: resolved "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== -ws@~8.2.3: - version "8.2.3" - resolved "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" - integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== xml2js@^0.4.17: version "0.4.23"