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

Order population expressions for HTML output #260

Merged
merged 6 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,24 @@ The following example of a proportion boolean measure shows how the logic highli
![Screenshot of Highlighting HTML Measure Logic](./static/logic-highlighting-example-2.png)

### CQL Statement Ordering in HTML
By default, the CQL statements in the generated HTML output are sorted into population statements, non-functions, and functions, respectively. Non-population statements appear in alphabetical order. To disable this behavior, use the `disableHTMLOrdering` calculation option.

By default, the CQL statements in the generated HTML output are sorted into population statements, non-functions, and functions, respectively. Non-population statements appear in alphabetical order. Population statements are sorted in the following order:

1. initial-population
2. denominator
3. denominator-exclusion
4. denominator-exception
5. numerator
6. numerator-exclusion
7. measure-population
8. measure-population-exclusion
9. measure-observation (alphabetically sorted if multiple)

The order of these populations is determined by most inclusive to least inclusive populations as shown in the following diagram found [here](http://hl7.org/fhir/us/cqfmeasures/STU1/measure-conformance.html#conformance-requirement-11):

![Screenshot of Population Venn Diagram](./static/population-diagram.png)
elsaperelli marked this conversation as resolved.
Show resolved Hide resolved

To disable this behavior, use the `disableHTMLOrdering` calculation option.

## Group Clause Coverage Highlighting

Expand Down
43 changes: 35 additions & 8 deletions src/calculation/HTMLBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Annotation, ELM } from '../types/ELMTypes';
import Handlebars from 'handlebars';
import { ClauseResult, DetailedPopulationGroupResult, ExecutionResult, StatementResult } from '../types/Calculator';
import { FinalResult, Relevance } from '../types/Enums';
import { FinalResult, PopulationType, Relevance } from '../types/Enums';
import mainTemplate from '../templates/main';
import clauseTemplate from '../templates/clause';
import { UnexpectedProperty, UnexpectedResource } from '../types/errors/CustomErrors';
Expand Down Expand Up @@ -92,23 +92,50 @@
*/
export function sortStatements(measure: fhir4.Measure, groupId: string, statements: StatementResult[]) {
const group = measure.group?.find(g => g.id === groupId) || measure.group?.[0];
const populationSet = new Set(group?.population?.map(p => p.criteria.expression));
const populationOrder = [
elsaperelli marked this conversation as resolved.
Show resolved Hide resolved
PopulationType.IPP,
PopulationType.DENOM,
PopulationType.DENEX,
PopulationType.DENEXCEP,
PopulationType.NUMER,
PopulationType.NUMEX,
PopulationType.MSRPOPL,
PopulationType.MSRPOPLEX,
PopulationType.OBSERV
];

// this is a lookup of cql expression identifier -> population type
const populationIdentifiers: Record<string, PopulationType> = {};
group?.population?.forEach(p => {
if (p.code?.coding?.[0].code !== undefined) {
populationIdentifiers[p.criteria.expression as string] = p.code.coding[0].code as PopulationType;
}
});

function populationCompare(a: StatementResult, b: StatementResult) {
return (
populationOrder.indexOf(populationIdentifiers[a.statementName]) -
populationOrder.indexOf(populationIdentifiers[b.statementName])
);
}

function alphaCompare(a: StatementResult, b: StatementResult) {
return a.statementName <= b.statementName ? -1 : 1;
}

statements.sort((a, b) => {
// if population statement, use population or send to beginning
if (a.statementName in populationIdentifiers) {
return b.statementName in populationIdentifiers ? populationCompare(a, b) : -1;
}
if (b.statementName in populationIdentifiers) return 1;

Check warning on line 131 in src/calculation/HTMLBuilder.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 131 in src/calculation/HTMLBuilder.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

// if function, alphabetize or send to end
if (a.isFunction) {
return b.isFunction ? alphaCompare(a, b) : 1;
}
if (b.isFunction) return -1;

// if population statement, leave order or send to beginning
if (populationSet.has(a.statementName)) {
return populationSet.has(b.statementName) ? 0 : -1;
}
if (populationSet.has(b.statementName)) return 1;

// if no function or population statement, alphabetize
return alphaCompare(a, b);
});
Expand Down
Binary file added static/population-diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 88 additions & 1 deletion test/unit/HTMLBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
cqlLogicClauseTrueStyle,
cqlLogicClauseFalseStyle,
cqlLogicClauseCoveredStyle,
calculateClauseCoverage
calculateClauseCoverage,
sortStatements
} from '../../src/calculation/HTMLBuilder';
import {
StatementResult,
Expand All @@ -28,6 +29,7 @@ describe('HTMLBuilder', () => {
const falseStyleString = objToCSS(cqlLogicClauseFalseStyle);
const coverageStyleString = objToCSS(cqlLogicClauseCoveredStyle);
const simpleMeasure = getJSONFixture('measure/simple-measure.json') as fhir4.Measure;
const cvMeasure = getJSONFixture('measure/cv-measure.json') as fhir4.Measure;
const singlePopMeasure = <fhir4.Measure>{
resourceType: 'Measure',
status: 'unknown',
Expand Down Expand Up @@ -303,4 +305,89 @@ describe('HTMLBuilder', () => {
expect(res.indexOf('ipp')).toBeLessThan(res.indexOf('SimpleVSRetrieve'));
expect(res.indexOf('SimpleVSRetrieve')).toBeLessThan(res.indexOf('A Function'));
});

test('sortStatements orders population statements in specified order, then other, then function for a proportion boolean measure', () => {
statementResults = [
{
libraryName: 'Test',
statementName: 'A Function',
localId: 'test-id-1',
final: FinalResult.FALSE,
relevance: Relevance.TRUE,
isFunction: true
},
{
libraryName: 'Test',
statementName: 'SimpleVSRetrieve',
localId: 'test-id-2',
final: FinalResult.FALSE,
relevance: Relevance.TRUE
},
{
libraryName: 'Test',
statementName: 'Numerator',
localId: 'test-id-3',
final: FinalResult.FALSE,
relevance: Relevance.TRUE
},
{
libraryName: 'Test',
statementName: 'Initial Population',
localId: 'test-id-4',
final: FinalResult.FALSE,
relevance: Relevance.TRUE
},
{
libraryName: 'Test',
statementName: 'Denominator Exclusion',
localId: 'test-id-5',
final: FinalResult.FALSE,
relevance: Relevance.TRUE
}
];
sortStatements(simpleMeasure, 'test', statementResults);
expect(statementResults[0].statementName === 'Initial Population');
expect(statementResults[1].statementName === 'Denominator Exclusion');
expect(statementResults[2].statementName === 'Numerator');
expect(statementResults[3].statementName === 'SimpleVSRetrieve');
expect(statementResults[4].statementName === 'A Function');
});

test('sortStatements orders population statements in specified order, then other, then function for a continuous-variable boolean measure', () => {
statementResults = [
{
libraryName: 'Test',
statementName: 'Measure Population Exclusions',
localId: 'test-id-1',
final: FinalResult.FALSE,
relevance: Relevance.TRUE
},
{
libraryName: 'Test',
statementName: 'Initial Population',
localId: 'test-id-2',
final: FinalResult.FALSE,
relevance: Relevance.TRUE
},
{
libraryName: 'Test',
statementName: 'MeasureObservation',
localId: 'test-id-3',
final: FinalResult.FALSE,
relevance: Relevance.TRUE,
isFunction: true
},
{
libraryName: 'Test',
statementName: 'Measure Population',
final: FinalResult.FALSE,
relevance: Relevance.TRUE
}
];
sortStatements(cvMeasure, 'test', statementResults);
expect(statementResults[0].statementName === 'Initial Population');
expect(statementResults[1].statementName === 'Measure Population');
expect(statementResults[2].statementName === 'Measure Population Exclusions');
expect(statementResults[3].statementName === 'MeasureObservation');
});
});