Skip to content

Commit

Permalink
Parse lab values that include a range (#452)
Browse files Browse the repository at this point in the history
  • Loading branch information
jean-the-coder committed Mar 18, 2024
1 parent bcffbb4 commit 1bcf4aa
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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));
Expand All @@ -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 => {
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/lib/models/resources/observation-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -21,6 +50,7 @@ describe('ObservationModel', () => {
});
});


describe('parsing unit', () => {
it('reads from valueQuantity.unit if set', () => {
let observation = new ObservationModel(observationR4Factory.build(), fhirVersions.R4);
Expand Down
80 changes: 68 additions & 12 deletions frontend/src/lib/models/resources/observation-model.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -55,29 +63,77 @@ 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 {
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");
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.
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;
private parseValueString(): ValueObject {
let matches = _.get(this.fhirResource, "valueString")?.match(/(?<value1>[\d.]*)?(?<operator>[^\d]*)?(?<value2>[\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.
Expand Down

0 comments on commit 1bcf4aa

Please sign in to comment.