From 1bcf4aaf7ed479bab33805ff02c478015a1d9b4a Mon Sep 17 00:00:00 2001 From: Jean Fernandez <55406257+jean-the-coder@users.noreply.github.com> Date: Mon, 18 Mar 2024 00:48:32 -0400 Subject: [PATCH] Parse lab values that include a range (#452) --- .../observation-bar-chart.component.ts | 17 ++-- .../observation-bar-chart.stories.ts | 6 ++ .../r4/resources/observation-r4-factory.ts | 12 +++ .../resources/observation-model.spec.ts | 30 +++++++ .../lib/models/resources/observation-model.ts | 80 ++++++++++++++++--- 5 files changed, 128 insertions(+), 17 deletions(-) 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 index ab005b4b..5317d777 100644 --- 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 @@ -15,7 +15,7 @@ const defaultChartEntryHeight = 30; styleUrls: ['./observation-bar-chart.component.scss'] }) export class ObservationBarChartComponent implements OnInit { - @Input() observations: [ObservationModel] + @Input() observations: ObservationModel[] chartHeight = defaultChartEntryHeight; @@ -122,14 +122,21 @@ export class ObservationBarChartComponent implements OnInit { return; } - let currentValues: number[] = [] + let currentValues = [] 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); + + let value = observation.value_object; + + if (value.range) { + currentValues.push([value.range.low, value.range.high]); + } else { + currentValues.push([value.value, value.value]) + } if (observation.effective_date) { this.barChartLabels.push(formatDate(observation.effective_date, "mediumDate", "en-US", undefined)); @@ -141,7 +148,7 @@ export class ObservationBarChartComponent implements OnInit { this.barChartData[1]['dataLabels'].push(observation.value_quantity_unit); } - let xAxisMax = Math.max(...currentValues) * 1.3; + let xAxisMax = Math.max(...currentValues.map(set => set[1])) * 1.3; this.barChartOptions.scales['x']['max'] = xAxisMax let updatedRefRanges = referenceRanges.map(range => { @@ -154,7 +161,7 @@ export class ObservationBarChartComponent implements OnInit { // @ts-ignore this.barChartData[0].data = updatedRefRanges - this.barChartData[1].data = currentValues.map(v => [v, v]) + this.barChartData[1].data = currentValues 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 index ccea23e5..de0e1e3f 100644 --- 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 @@ -33,6 +33,12 @@ export const NoRange: Story = { } }; +export const ValueStringWithRange: Story = { + args: { + observations: [new ObservationModel(observationR4Factory.valueString('<10 IntlUnit/mL').referenceRangeOnlyHigh(50).build(), fhirVersions.R4)] + } +}; + export const Range: Story = { args: { observations: [new ObservationModel(observationR4Factory.referenceRange().build(), fhirVersions.R4)] 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 index 5c85d375..7a7f7340 100644 --- a/frontend/src/lib/fixtures/factories/r4/resources/observation-r4-factory.ts +++ b/frontend/src/lib/fixtures/factories/r4/resources/observation-r4-factory.ts @@ -9,6 +9,18 @@ class ObservationR4Factory extends Factory<{}> { }) } + valueQuantity(params: {}) { + return this.params({ + valueQuantity: { + value: params['value'] || 6.3, + unit: params['unit'] || 'mmol/l', + system: 'http://unitsofmeasure.org', + code: params['code'] || 'mmol/L', + comparator: params['comparator'] + } + }) + } + referenceRange(high?: number, low?: number) { return this.params({ referenceRange: [ diff --git a/frontend/src/lib/models/resources/observation-model.spec.ts b/frontend/src/lib/models/resources/observation-model.spec.ts index ace3bb7c..9f30f71a 100644 --- a/frontend/src/lib/models/resources/observation-model.spec.ts +++ b/frontend/src/lib/models/resources/observation-model.spec.ts @@ -8,6 +8,35 @@ describe('ObservationModel', () => { }); describe('parsing value', () => { + it('reads from valueQuantity.value if set', () => { + let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4); + + expect(observation.value_object.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_object.value).toEqual(5.5); + }); + + it('parses value correctly when valueQuantity.comparator is set', () => { + let observation = new ObservationModel(observationR4Factory.valueQuantity({ comparator: '<', value: 8 }).build(), fhirVersions.R4); + let observation2 = new ObservationModel(observationR4Factory.valueQuantity({ comparator: '>', value: 8 }).build(), fhirVersions.R4); + + expect(observation.value_object).toEqual({ range: { low: null, high: 8 } }); + expect(observation2.value_object).toEqual({ range: { low: 8, high: null } }); + }); + + it('parses value correctly when valueString has a range', () => { + let observation = new ObservationModel(observationR4Factory.valueString('<10 IntlUnit/mL').build(), fhirVersions.R4); + let observation2 = new ObservationModel(observationR4Factory.valueString('>10 IntlUnit/mL').build(), fhirVersions.R4); + + expect(observation.value_object).toEqual({ range: { low: null, high: 10 } }); + expect(observation2.value_object).toEqual({ range: { low: 10, high: null } }); + }); + + // following two tests being kept temporarily. will be removed in next PR when I remove value_quantity_value it('reads from valueQuantity.value if set', () => { let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4); @@ -21,6 +50,7 @@ describe('ObservationModel', () => { }); }); + describe('parsing unit', () => { it('reads from valueQuantity.unit if set', () => { let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4); diff --git a/frontend/src/lib/models/resources/observation-model.ts b/frontend/src/lib/models/resources/observation-model.ts index ca3b1d34..06ea5101 100644 --- a/frontend/src/lib/models/resources/observation-model.ts +++ b/frontend/src/lib/models/resources/observation-model.ts @@ -1,6 +1,6 @@ import {fhirVersions, ResourceType} from '../constants'; import * as _ from "lodash"; -import {CodableConceptModel, hasValue} from '../datatypes/codable-concept-model'; +import {CodableConceptModel} from '../datatypes/codable-concept-model'; import {ReferenceModel} from '../datatypes/reference-model'; import {FastenDisplayModel} from '../fasten/fasten-display-model'; import {FastenOptions} from '../fasten/fasten-options'; @@ -10,12 +10,19 @@ interface referenceRangeHash { high: number | null } +// should have one or the other +export interface ValueObject { + range?: { low?: number | null, high?: number | null } + value?: number | string | boolean | null +} + export class ObservationModel extends FastenDisplayModel { code: CodableConceptModel | undefined effective_date: string code_coding_display: string code_text: string - value_quantity_value: number + value_object: ValueObject + value_quantity_value value_quantity_unit: string status: string value_codeable_concept_text: string @@ -34,7 +41,8 @@ export class ObservationModel extends FastenDisplayModel { 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 = this.parseValue(); + this.value_object = this.parseValue(); + this.value_quantity_value = this.value_object?.value; this.value_quantity_unit = this.parseUnit(); this.status = _.get(fhirResource, 'status', ''); this.value_codeable_concept_text = _.get( @@ -55,9 +63,8 @@ export class ObservationModel extends FastenDisplayModel { 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 parseValue(): ValueObject { + return this.parseValueQuantity() || this.parseValueString() } private parseUnit(): string { @@ -65,9 +72,23 @@ export class ObservationModel extends FastenDisplayModel { } // 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"); + private parseValueQuantity(): ValueObject { + let quantity = _.get(this.fhirResource, "valueQuantity"); + + if (!quantity) { + return null; + } + + switch (quantity.comparator) { + case '<': + case '<=': + return { range: { low: null, high: quantity.value } }; + case '>': + case '>=': + return { range: { low: quantity.value, high: null } }; + default: + return { value: quantity.value } + } } // Look for the observation's numeric value. Use this first before valueStringUnit which is a backup if this can't be found. @@ -75,9 +96,44 @@ export class ObservationModel extends FastenDisplayModel { 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; + private parseValueString(): ValueObject { + let matches = _.get(this.fhirResource, "valueString")?.match(/(?[\d.]*)?(?[^\d]*)?(?[\d.]*)?/) + + if(!matches) { + return { range: { low: null, high: null } } + } + + if (!!matches.groups['value1'] && !!matches.groups['value2']) { + return { + range: { + low: parseFloat(matches.groups['value1']), + high: parseFloat(matches.groups['value2']) + } + } + } + + if (['<', '<='].includes(matches.groups['operator'])) { + return { + range: { + low: null, + high: parseFloat(matches.groups['value2']) + } + } + } else if (['>', '>='].includes(matches.groups['operator'])) { + return { + range: { + low: parseFloat(matches.groups['value2']), + high: null + } + } + } + let float = parseFloat(matches.groups['value1']); + + if (Number.isNaN(float)) { + return { value: matches.groups['value1'] } + } + + return { value: float }; } // Use if valueUnit can't be found.