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
This commit is contained in:
Jean Fernandez 2024-03-15 19:00:43 -04:00 committed by GitHub
parent 43579df659
commit bcffbb4769
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 785 additions and 436 deletions

8
.gitignore vendored
View File

@ -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

26
.vscode/launch.json vendored Normal file
View File

@ -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"
}
}
]
}

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"karmaTestExplorer.karmaConfFilePath": "frontend/karma.conf.js",
"karmaTestExplorer.projectWorkspaces": [
"frontend"
]
}

View File

@ -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']
},
},
});
};

View File

@ -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",

View File

@ -0,0 +1,9 @@
<!-- surrounding div needed so chart can resize when window size changes -->
<div class="observation-bar-chart-container">
<canvas baseChart
[height]="chartHeight"
[type]="'bar'"
[datasets]="barChartData"
[labels]="barChartLabels"
[options]="barChartOptions"></canvas>
</div>

View File

@ -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<ObservationBarChartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ ObservationBarChartComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(ObservationBarChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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)
}
}

View File

@ -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<ObservationBarChartComponent> = {
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<ObservationBarChartComponent>;
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)]
}
};

View File

@ -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,

View File

@ -14,13 +14,7 @@
<p class="az-content-text mg-b-20">Observations are a central element in healthcare, used to support diagnosis, monitor progress, determine baselines and patterns and even capture demographic characteristics.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
<canvas baseChart
[height]="chartHeight"
[type]="'bar'"
[datasets]="barChartData"
[labels]="barChartLabels"
[options]="barChartOptions"
></canvas>
<observation-bar-chart [observations]="[displayModel]"></observation-bar-chart>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" [routerLink]="['/explore', displayModel?.source_id, 'resource', displayModel?.source_resource_id]">details</a>

View File

@ -1,19 +1,17 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {CommonModule, formatDate} from '@angular/common';
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
import {CommonModule} from '@angular/common';
import {BadgeComponent} from '../../common/badge/badge.component';
import {TableComponent} from '../../common/table/table.component';
import {Router, RouterModule} from '@angular/router';
import {LocationModel} from '../../../../../lib/models/resources/location-model';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {ObservationModel} from '../../../../../lib/models/resources/observation-model';
import {ChartConfiguration} from 'chart.js';
import fhirpath from 'fhirpath';
import {NgChartsModule} from 'ng2-charts';
import { ObservationBarChartComponent } from 'src/app/components/fhir-card/common/observation-bar-chart/observation-bar-chart.component';
@Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, RouterModule, NgChartsModule],
imports: [CommonModule, BadgeComponent, TableComponent, RouterModule, NgbCollapseModule, ObservationBarChartComponent],
providers: [],
selector: 'fhir-observation',
templateUrl: './observation.component.html',
styleUrls: ['./observation.component.scss']
@ -25,81 +23,6 @@ export class ObservationComponent implements OnInit {
tableData: TableRowItem[] = []
//observation chart data
chartHeight = 45
barChartData =[
{
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: {
}
}
] as ChartConfiguration<'bar'>['data']['datasets']
barChartLabels = [] // ["2020", "2018"] //["1","2","3","4","5","6","7","8"]
barChartOptions = {
indexAxis: 'y',
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,
// 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(public changeRef: ChangeDetectorRef, public router: Router) { }
ngOnInit(): void {
@ -107,7 +30,8 @@ export class ObservationComponent implements OnInit {
return
}
this.tableData.push( {
this.tableData.push(
{
label: 'Issued on',
data: this.displayModel?.effective_date,
enabled: !!this.displayModel?.effective_date,
@ -131,30 +55,13 @@ export class ObservationComponent implements OnInit {
},
{
label: 'Reference',
data: [this.displayModel?.reference_range?.[0]?.low?.value,this.displayModel?.reference_range?.[0]?.high?.value].join(" "),
data: this.displayModel.referenceRangeDisplay(),
enabled: !!this.displayModel?.reference_range,
})
//populate chart data
if(this.displayModel?.effective_date) {
this.barChartLabels.push(
formatDate(this.displayModel?.effective_date, "mediumDate", "en-US", undefined)
)
} else {
this.barChartLabels.push("")
}
this.barChartData[0].data = [[this.displayModel?.reference_range?.[0]?.low?.value, this.displayModel?.reference_range?.[0]?.high?.value]]
this.barChartData[1].data = [[this.displayModel?.value_quantity_value as number, this.displayModel?.value_quantity_value as number]]
let suggestedMax = (this.displayModel?.value_quantity_value as number) * 1.1;
this.barChartOptions.scales['x']['suggestedMax'] = suggestedMax
console.log("Observation chart data: ", this.barChartData[0].data, this.barChartData[1].data)
}
)
}
markForCheck(){
this.changeRef.markForCheck()
}
}

View File

@ -1,24 +1,18 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {fhirVersions} from "../../../../../lib/models/constants";
import R4Example1Json from "../../../../../lib/fixtures/r4/resources/observation/example1.json";
import R4Example2Json from "../../../../../lib/fixtures/r4/resources/observation/example2.json";
import R4Example3Json from "../../../../../lib/fixtures/r4/resources/observation/example3.json";
import {ObservationComponent} from "./observation.component";
import {ObservationModel} from "../../../../../lib/models/resources/observation-model";
import { moduleMetadata } from '@storybook/angular';
import { fhirVersions } from "../../../../../lib/models/constants";
import { ObservationComponent } from "./observation.component";
import { ObservationModel } from "../../../../../lib/models/resources/observation-model";
import { RouterTestingModule } from '@angular/router/testing';
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<ObservationComponent> = {
title: 'Fhir Card/Observation',
component: ObservationComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
moduleMetadata({
imports: [ RouterTestingModule ]
})
],
tags: ['autodocs'],
render: (args: ObservationComponent) => ({
@ -40,30 +34,11 @@ const meta: Meta<ObservationComponent> = {
export default meta;
type Story = StoryObj<ObservationComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
let r4Example1DisplayModel = new ObservationModel(R4Example1Json, fhirVersions.R4)
r4Example1DisplayModel.source_id = '123-456-789'
r4Example1DisplayModel.source_resource_id = '123-456-789'
export const R4Example1: Story = {
let observation = new ObservationModel(observationR4Factory.referenceRange().build(), fhirVersions.R4);
observation.source_id = '123-456-789'
observation.source_resource_id = '123-456-789'
export const Entry: Story = {
args: {
displayModel: r4Example1DisplayModel
}
};
let r4Example2DisplayModel = new ObservationModel(R4Example2Json, fhirVersions.R4)
r4Example2DisplayModel.source_id = '123-456-789'
r4Example2DisplayModel.source_resource_id = '123-456-789'
export const R4Example2: Story = {
args: {
displayModel: r4Example2DisplayModel
}
};
let r4Example3DisplayModel = new ObservationModel(R4Example3Json, fhirVersions.R4)
r4Example3DisplayModel.source_id = '123-456-789'
r4Example3DisplayModel.source_resource_id = '123-456-789'
export const R4Example3: Story = {
args: {
displayModel: r4Example3DisplayModel
displayModel: observation
}
};

View File

@ -36,12 +36,13 @@
<div #collapse="ngbCollapse" [ngbCollapse]="true">
<ul>
<li class="cursor-pointer tx-indigo" *ngFor="let observation of observations" [routerLink]="['/explore', observation?.source_id, 'resource', observation?.source_resource_id]">Observation: {{observation | fhirPath: "Observation.effectiveDateTime": "Observation.issued" | date}}</li>
<li class="cursor-pointer tx-indigo" *ngFor="let observation of observations">
<a [routerLink]="['/explore', observation?.source_id, 'resource', observation?.source_resource_id]">
Observation: {{observation | fhirPath: "Observation.effectiveDateTime": "Observation.issued" | date}}
</a>
</li>
</ul>
</div>
</div>
</div>
@ -53,15 +54,12 @@
</div>
<div class="col-12">
<canvas baseChart [height]="chartHeight" [type]="'bar'" [datasets]="barChartData" [labels]="barChartLabels" [options]="barChartOptions"></canvas>
<observation-bar-chart [observations]="observationModels"></observation-bar-chart>
</div>
</div>
</div>
</div>
</div><!-- card-body -->
</div>

View File

@ -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();

View File

@ -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))
}
}

View File

@ -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<any> = (storyFunc, context) => {
@ -39,8 +40,8 @@ const meta: Meta<ReportLabsObservationComponent> = {
decorators: [
withHttpClientProvider,
moduleMetadata({
imports: [PipesModule, GlossaryLookupComponent, NgChartsModule, RouterTestingModule, HttpClientModule],
declarations: [NgbCollapse],
imports: [PipesModule, GlossaryLookupComponent, NgChartsModule, RouterTestingModule, HttpClientModule, ObservationBarChartComponent],
declarations: [ NgbCollapse ],
providers: [],
})
],

View File

@ -248,8 +248,8 @@ describe('DashboardWidgetComponent', () => {
});
})
describe('Vitals - ListWidget', () => {})
describe('Resource Aggregation - DonutWidget', () => {})
// describe('Vitals - ListWidget', () => {})
// describe('Resource Aggregation - DonutWidget', () => {})
})

View File

@ -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;

View File

@ -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: "<div xmlns=\'http://www.w3.org/1999/xhtml\'><p><b>Generated Narrative with Details</b></p><p><b>id</b>: example</p><p><b>status</b>: final</p><p><b>category</b>: Vital Signs <span>(Details : {http://terminology.hl7.org/CodeSystem/observation-category code 'vital-signs' = 'Vital Signs', given as 'Vital Signs'})</span></p><p><b>code</b>: Body Weight <span>(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'})</span></p><p><b>subject</b>: <a>Patient/example</a></p><p><b>encounter</b>: <a>Encounter/example</a></p><p><b>effective</b>: 28/03/2016</p><p><b>value</b>: 185 lbs<span> (Details: UCUM code [lb_av] = 'lb_av')</span></p></div>"
},
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'
}
]
}
],
}
));

View File

@ -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)
}
});
});
});
});

View File

@ -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(/(?<value>[\d.]*)(?<text>.*)/).groups.value;
}
// Use if valueUnit can't be found.
private valueStringUnit(): string {
return _.get(this.fhirResource, "valueString")?.match(/(?<value>[\d.]*)(?<text>.*)/).groups.text;
}
private referenceRangeFromString(str: string): referenceRangeHash {
let matches = str?.match(/(?<value1>[\d.]*)?(?<operator>[^\d]*)?(?<value2>[\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 '';
}
}

View File

@ -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"