Skip to content

Commit

Permalink
Parse reference ranges that use < and > (#450)
Browse files Browse the repository at this point in the history
* Add vscode config

* Comment out empty tests causing issues running tests in vscode

* Add Fishery, update test dependencies, update chart.js

* Add factory for building fhir r4 observation object

* Fix deprecation warnings in _mixins.scss

* Update observation model for better value and reference range parsing

* Add observation-bar-chart.component to pull out bar chart logic into reusable component

* Use new component in observation resource and report lab component
  • Loading branch information
jean-the-coder committed Mar 15, 2024
1 parent 43579df commit bcffbb4
Show file tree
Hide file tree
Showing 24 changed files with 785 additions and 436 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ cmake-build-release/
# IntelliJ
out/

# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

# mpeltonen/sbt-idea plugin
.idea_modules/

Expand Down Expand Up @@ -70,3 +77,4 @@ fasten.db-shm
fasten.db-wal

backend/resources/related_versions.json
frontend/documentation.json
26 changes: 26 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Go Tests",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}/backend",
"args": [
"-test.run"
]
},
{
"type": "chrome",
"request": "attach",
"name": "Attach Karma Chrome",
"address": "localhost",
"port": 9333,
"pathMapping": {
"/": "${workspaceRoot}/frontend",
"/base/": "${workspaceRoot}/frontend"
}
}
]
}
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"karmaTestExplorer.karmaConfFilePath": "frontend/karma.conf.js",
"karmaTestExplorer.projectWorkspaces": [
"frontend"
]
}
4 changes: 2 additions & 2 deletions frontend/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ module.exports = function(config) {
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox', '--disable-gpu']
}
flags: ['--no-sandbox', '--disable-gpu', '--remote-debugging-port=9333']
},
},
});
};
17 changes: 9 additions & 8 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@webcomponents/webcomponentsjs": "^2.8.0",
"asmcrypto.js": "^2.3.2",
"bootstrap": "^4.4.1",
"chart.js": "^4.0.1",
"chart.js": "^4.4.2",
"dwv": "^0.31.0",
"fhirpath": "^3.3.0",
"gridstack": "8.1.1",
Expand Down Expand Up @@ -77,14 +77,15 @@
"@types/jasminewd2": "~2.0.3",
"chromatic": "^6.19.8",
"codelyzer": "^5.1.2",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "^2.2.0",
"fishery": "^2.2.2",
"jasmine-core": "~5.1.2",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.4.3",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "^2.2.1",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",
"protractor": "~7.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- surrounding div needed so chart can resize when window size changes -->
<div class="observation-bar-chart-container">
<canvas baseChart
[height]="chartHeight"
[type]="'bar'"
[datasets]="barChartData"
[labels]="barChartLabels"
[options]="barChartOptions"></canvas>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ObservationBarChartComponent } from './observation-bar-chart.component';

describe('ObservationBarChartComponent', () => {
let component: ObservationBarChartComponent;
let fixture: ComponentFixture<ObservationBarChartComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ ObservationBarChartComponent ]
})
.compileComponents();

fixture = TestBed.createComponent(ObservationBarChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Component, Input, OnInit } from '@angular/core';
import { formatDate } from '@angular/common';
import { ObservationModel } from '../../../../../lib/models/resources/observation-model';
import { ChartConfiguration } from 'chart.js';
import { NgChartsModule } from 'ng2-charts';

const defaultChartHeight = 65;
const defaultChartEntryHeight = 30;

@Component({
standalone: true,
selector: 'observation-bar-chart',
imports: [ NgChartsModule ],
templateUrl: './observation-bar-chart.component.html',
styleUrls: ['./observation-bar-chart.component.scss']
})
export class ObservationBarChartComponent implements OnInit {
@Input() observations: [ObservationModel]

chartHeight = defaultChartEntryHeight;

// based on https://stackoverflow.com/questions/38889716/chartjs-2-stacked-bar-with-marker-on-top
// https://stackoverflow.com/questions/62711919/chart-js-horizontal-lines-per-bar
barChartData =[
{
label: "Reference",
data: [],
dataLabels: [],
backgroundColor: "rgba(91, 71, 251,0.6)",
hoverBackgroundColor: "rgba(91, 71, 251,0.2)",
parsing: {
xAxisKey: 'range'
},
tooltip: {
callbacks: {
label: function(context) {
return `${context.dataset.label}: ${context.dataset.dataLabels[context.dataIndex]}`;
}
}
}
},
{
label: "Result",
data: [],
// @ts-ignore
dataLabels: [],
borderColor: "rgba(0,0,0,1)",
backgroundColor: "rgba(0,0,0,1)",
hoverBackgroundColor: "rgba(0,0,0,1)",
minBarLength: 3,
barPercentage: 1,
parsing: {
xAxisKey: 'value'
},
// @ts-ignore
tooltip: {
callbacks: {
label: function(context) {
let label = `${context.dataset.label}: ${context.parsed.x}`;

if (context.dataset.dataLabels[context.dataIndex]) {
return `${label} ${context.dataset.dataLabels[context.dataIndex]}`;
}
return label;
}
}
}
}
] as ChartConfiguration<'bar'>['data']['datasets']

barChartLabels = [] // ["2020", "2018"] //["1","2","3","4","5","6","7","8"]

barChartOptions = {
indexAxis: 'y',
maintainAspectRatio: false,
legend:{
display: false,
},
autoPadding: true,
//add padding to fix tooltip cutoff
layout: {
padding: {
left: 0,
right: 4,
top: 0,
bottom: 10
}
},
scales: {
y: {
stacked: true,
ticks: {
beginAtZero: true,
fontSize: 10,
min: 0,
},
},
x: {
scaleLabel:{
display: false,
padding: 4,
},
ticks: {
beginAtZero: true,
fontSize: 10,
min: 0,
},
},
}
} as ChartConfiguration<'bar'>['options']

barChartColors = [
{
backgroundColor: 'white'
}
];

constructor() { }

ngOnInit(): void {
if(!this.observations || !this.observations[0]) {
return;
}

let currentValues: number[] = []
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);

if (observation.effective_date) {
this.barChartLabels.push(formatDate(observation.effective_date, "mediumDate", "en-US", undefined));
} else {
this.barChartLabels.push('Unknown date');
}

this.barChartData[0]['dataLabels'].push(observation.referenceRangeDisplay());
this.barChartData[1]['dataLabels'].push(observation.value_quantity_unit);
}

let xAxisMax = Math.max(...currentValues) * 1.3;
this.barChartOptions.scales['x']['max'] = xAxisMax

let updatedRefRanges = referenceRanges.map(range => {
if (range[0] && !range[1]) {
return [range[0], xAxisMax]
} else {
return [range[0], range[1]]
}
});

// @ts-ignore
this.barChartData[0].data = updatedRefRanges
this.barChartData[1].data = currentValues.map(v => [v, v])

this.chartHeight = defaultChartHeight + (defaultChartEntryHeight * currentValues.length)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { fhirVersions } from "../../../../../lib/models/constants";
import { ObservationBarChartComponent } from './observation-bar-chart.component';
import { ObservationModel } from 'src/lib/models/resources/observation-model';
import { observationR4Factory } from 'src/lib/fixtures/factories/r4/resources/observation-r4-factory';

// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<ObservationBarChartComponent> = {
title: 'Fhir Card/Common/ObservationBarChart',
component: ObservationBarChartComponent,
decorators: [
],
tags: ['autodocs'],
render: (args: ObservationBarChartComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
observations: {
control: 'object',
}
},
};

export default meta;
type Story = StoryObj<ObservationBarChartComponent>;

export const NoRange: Story = {
args: {
observations: [new ObservationModel(observationR4Factory.build(), fhirVersions.R4)]
}
};

export const Range: Story = {
args: {
observations: [new ObservationModel(observationR4Factory.referenceRange().build(), fhirVersions.R4)]
}
};

export const RangeOnlyLow: Story = {
args: {
observations: [new ObservationModel(observationR4Factory.referenceRangeOnlyLow().build(), fhirVersions.R4)]
}
};

export const RangeOnlyLowText: Story = {
args: {
observations: [new ObservationModel(observationR4Factory.referenceRangeStringOnlyLow().build(), fhirVersions.R4)]
}
};

export const RangeOnlyHigh: Story = {
args: {
observations: [new ObservationModel(observationR4Factory.referenceRangeOnlyHigh().build(), fhirVersions.R4)]
}
};

export const RangeOnlyHighText: Story = {
args: {
observations: [new ObservationModel(observationR4Factory.referenceRangeStringOnlyHigh().build(), fhirVersions.R4)]
}
};
3 changes: 3 additions & 0 deletions frontend/src/app/components/fhir-card/fhir-card.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {FhirCardComponent} from './fhir-card/fhir-card.component';
import {FhirCardOutletDirective} from './fhir-card/fhir-card-outlet.directive';
import { EncounterComponent } from './resources/encounter/encounter.component';
import { RtfComponent } from './datatypes/rtf/rtf.component';
import { ObservationBarChartComponent } from './common/observation-bar-chart/observation-bar-chart.component';



Expand All @@ -36,6 +37,7 @@ import { RtfComponent } from './datatypes/rtf/rtf.component';
//common
CommonModule,
BadgeComponent,
ObservationBarChartComponent,
//datatypes
TableComponent,
BinaryTextComponent,
Expand Down Expand Up @@ -75,6 +77,7 @@ import { RtfComponent } from './datatypes/rtf/rtf.component';
//common
BadgeComponent,
TableComponent,
ObservationBarChartComponent,
//datatypes
BinaryTextComponent,
CodableConceptComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ <h6 class="card-title">{{displayModel?.sort_title}}</h6>
<p class="az-content-text mg-b-20">Observations are a central element in healthcare, used to support diagnosis, monitor progress, determine baselines and patterns and even capture demographic characteristics.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>

<canvas baseChart
[height]="chartHeight"
[type]="'bar'"
[datasets]="barChartData"
[labels]="barChartLabels"
[options]="barChartOptions"
></canvas>
<observation-bar-chart [observations]="[displayModel]"></observation-bar-chart>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" [routerLink]="['/explore', displayModel?.source_id, 'resource', displayModel?.source_resource_id]">details</a>
Expand Down
Loading

0 comments on commit bcffbb4

Please sign in to comment.