Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse lab values that include a range #452

Merged
merged 1 commit into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Copy link
Member

@AnalogJ AnalogJ Mar 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦 - good catch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, hey, that 🤦is mine. I'm the one who made this file 😆

I just find it interesting that it all worked regardless. I only noticed it because I was doing some testing in my other branch that has a lot more change going on and storybook finally complained near the end of my changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha, don't be too hard on yourself, I've definitely done stuff like that -- I thought you copy pasted this from my original file :D

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