Parse lab values that include a range (#452)

This commit is contained in:
Jean Fernandez 2024-03-18 00:48:32 -04:00 committed by GitHub
parent bcffbb4769
commit 1bcf4aaf7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 128 additions and 17 deletions

View File

@ -15,7 +15,7 @@ const defaultChartEntryHeight = 30;
styleUrls: ['./observation-bar-chart.component.scss'] styleUrls: ['./observation-bar-chart.component.scss']
}) })
export class ObservationBarChartComponent implements OnInit { export class ObservationBarChartComponent implements OnInit {
@Input() observations: [ObservationModel] @Input() observations: ObservationModel[]
chartHeight = defaultChartEntryHeight; chartHeight = defaultChartEntryHeight;
@ -122,14 +122,21 @@ export class ObservationBarChartComponent implements OnInit {
return; return;
} }
let currentValues: number[] = [] let currentValues = []
let referenceRanges = [] let referenceRanges = []
for(let observation of this.observations) { for(let observation of this.observations) {
let refRange = observation.reference_range; let refRange = observation.reference_range;
referenceRanges.push([refRange.low || 0, refRange.high || 0]); 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) { if (observation.effective_date) {
this.barChartLabels.push(formatDate(observation.effective_date, "mediumDate", "en-US", undefined)); 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); 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 this.barChartOptions.scales['x']['max'] = xAxisMax
let updatedRefRanges = referenceRanges.map(range => { let updatedRefRanges = referenceRanges.map(range => {
@ -154,7 +161,7 @@ export class ObservationBarChartComponent implements OnInit {
// @ts-ignore // @ts-ignore
this.barChartData[0].data = updatedRefRanges 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) this.chartHeight = defaultChartHeight + (defaultChartEntryHeight * currentValues.length)
} }

View File

@ -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 = { export const Range: Story = {
args: { args: {
observations: [new ObservationModel(observationR4Factory.referenceRange().build(), fhirVersions.R4)] observations: [new ObservationModel(observationR4Factory.referenceRange().build(), fhirVersions.R4)]

View File

@ -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) { referenceRange(high?: number, low?: number) {
return this.params({ return this.params({
referenceRange: [ referenceRange: [

View File

@ -8,6 +8,35 @@ describe('ObservationModel', () => {
}); });
describe('parsing value', () => { 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', () => { it('reads from valueQuantity.value if set', () => {
let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4); let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4);
@ -21,6 +50,7 @@ describe('ObservationModel', () => {
}); });
}); });
describe('parsing unit', () => { describe('parsing unit', () => {
it('reads from valueQuantity.unit if set', () => { it('reads from valueQuantity.unit if set', () => {
let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4); let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4);

View File

@ -1,6 +1,6 @@
import {fhirVersions, ResourceType} from '../constants'; import {fhirVersions, ResourceType} from '../constants';
import * as _ from "lodash"; 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 {ReferenceModel} from '../datatypes/reference-model';
import {FastenDisplayModel} from '../fasten/fasten-display-model'; import {FastenDisplayModel} from '../fasten/fasten-display-model';
import {FastenOptions} from '../fasten/fasten-options'; import {FastenOptions} from '../fasten/fasten-options';
@ -10,12 +10,19 @@ interface referenceRangeHash {
high: number | null 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 { export class ObservationModel extends FastenDisplayModel {
code: CodableConceptModel | undefined code: CodableConceptModel | undefined
effective_date: string effective_date: string
code_coding_display: string code_coding_display: string
code_text: string code_text: string
value_quantity_value: number value_object: ValueObject
value_quantity_value
value_quantity_unit: string value_quantity_unit: string
status: string status: string
value_codeable_concept_text: string value_codeable_concept_text: string
@ -34,7 +41,8 @@ export class ObservationModel extends FastenDisplayModel {
this.code = _.get(fhirResource, 'code'); this.code = _.get(fhirResource, 'code');
this.code_coding_display = _.get(fhirResource, 'code.coding.0.display'); this.code_coding_display = _.get(fhirResource, 'code.coding.0.display');
this.code_text = _.get(fhirResource, 'code.text', ''); 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.value_quantity_unit = this.parseUnit();
this.status = _.get(fhirResource, 'status', ''); this.status = _.get(fhirResource, 'status', '');
this.value_codeable_concept_text = _.get( this.value_codeable_concept_text = _.get(
@ -55,9 +63,8 @@ export class ObservationModel extends FastenDisplayModel {
this.subject = _.get(fhirResource, 'subject'); this.subject = _.get(fhirResource, 'subject');
} }
private parseValue(): number { private parseValue(): ValueObject {
// TODO: parseFloat would return NaN if it can't parse. Need to check and make sure that doesn't cause issues return this.parseValueQuantity() || this.parseValueString()
return this.valueQuantity() || parseFloat(this.valueString())
} }
private parseUnit(): string { 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. // 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 { private parseValueQuantity(): ValueObject {
// debugger let quantity = _.get(this.fhirResource, "valueQuantity");
return _.get(this.fhirResource, "valueQuantity.value");
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. // 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"); 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 parseValueString(): ValueObject {
private valueString(): string { let matches = _.get(this.fhirResource, "valueString")?.match(/(?<value1>[\d.]*)?(?<operator>[^\d]*)?(?<value2>[\d.]*)?/)
return _.get(this.fhirResource, "valueString")?.match(/(?<value>[\d.]*)(?<text>.*)/).groups.value;
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. // Use if valueUnit can't be found.