Parse lab values that include a range (#452)
This commit is contained in:
parent
bcffbb4769
commit
1bcf4aaf7e
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue