CDA Export - Custom Narrative Generation Tutorial
When generating a CDA Document using the CDA Exchange+ module a narrative is generated to populate the <text>
element of each section.
This narrative can be edited to display the information necessary from the various resources it consists of.
This tutorial aims to help you familiarise yourself with the various ways narrative generation occurs in the CDA Exchange+ Module and how it can be customized to suit your needs. A global overview of all the features can also be found (here)[/docs/cda_exchange_plus/narrative_generation.html).
By default, if the Composition.section.text
has been populated (be it manually or via generation) it will use its contents to populate the <text>
element for the corresponding CDA section. Generally, this generated narrative will appear as a simple tabular summary of the most relevant fields of the entries.
If the simple tabular summary is not sufficient, custom templates can be uploaded to use instead; these will override the default narrative templates using the Narrative Generator module. The following setup should be followed:
Properties file:
module.narrative_generator.type=NARRATIVE_GENERATOR
module.narrative_generator.config.manifest_file={path_to_cda-narratives.properties}
Yaml file:
{module_id}:
name: Narrative Generator
enabled: true
type: NARRATIVE_GENERATOR
config:
manifest_file={path_to_cda-narratives.properties}
Path_to_cda-narratives.properties
is the location of the cda-narratives.properties file, this can be on the classpath:
but can also be found via a local file path using file:/path/to/file.txt
. How this file is set up can be found later in the tutorial.
Generally speaking it’s good practice to put this file in the config_seeding
folder in the classes/
folder, Ie. classpath:config_seeding/cda-narrative.properties
Once this file has been loaded, the CDA Exchange+ Module
needs to be updated with a Dependency to the Narrative Generator
module.
The cda-narrative.properties file specifies where the narrative generator module can find the templates for each section. Every section will require the following format:
{resource}.resourceType = Bundle
{resource}.tag = {resource_tag}
{resource}.narrative = {path-to-resource-html}
The resource_tag
corresponds with the code value of the section as defined by the CDA Specification . Generally speaking there should be a specific LOINC code associated with the section.
The resource.html
will be a thymeleaf template that indicates what needs to be generated. This will be discussed in the next section.
It is recommended to put the resource.html
files in the same config_seeding/
folder as the cda-narratives.properties
file. Ie. classpath:config_seeding/resource.html
The html for each FHIR resource is written in Thymeleaf and uses FhirPath to select which part of the resource needs to be displayed.
Below is an example of a Thymeleaf template for Encounter resources. It will display the resources via a table with the following fields: title, status, class, type, priority, period, location and participant.
Each narrative gets a Bundle as input
Using <th:block th:each=”entry : ${resource.entry}” th:object=”${entry.getResource()}”>
will unpack each resource from the Bundle Entries
the resource can be retrieved from the entry and <th:block th:if="*{getResourceType().name() == 'Encounter'}">
is used to validate that this template only triggers on Encounter resources.
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:if="*{getResourceType().name() == 'Encounter'}">
Once the Encounter resource has been established, FhirPath queries can be leveraged to traverse the resource and resolve references. For example:
<td th:text="*{getDiagnosis().isEmpty() != true ? #fhirpath.evaluate(#object, 'diagnosis.condition.resolve().code.coding[0].display')[0] : 'No Diagnosis'}">Title</td>
The above snippet will populate the <td> element with the Diagnosis, it will first check if the diagnosis field is not empty (getDiagnosis().isEmpty())
, after that, it uses FhirPath to find the condition,
resolves the Reference to a Condition/Procedure and retrieve the first occurring coding.display
, if the diagnosis is empty No Diagnosis
is displayed.
For more information on how to use Fhirpath: here
The full Encounter resource Thymeleaf below can serve as a starting point when creating your own templates
<div xmlns:th="http://www.thymeleaf.org">
<h5>Encounters</h5>
<table>
<thead>
<tr>
<th>Title</th>
<th>Status</th>
<th>Class</th>
<th>Type</th>
<th>Priority</th>
<th>Period</th>
<th>Location</th>
<th>Participant</th>
</tr>
</thead>
<tbody>
<th:block th:each="entry : ${resource.entry}" th:object="${entry.getResource()}">
<th:block th:if="*{getResourceType().name() == 'Encounter'}">
<tr>
<td th:text="*{getDiagnosis().isEmpty() != true ? #fhirpath.evaluate(#object, 'diagnosis.condition.resolve().code.coding[0].display')[0] : 'No Diagnosis'}">Title</td>
<td th:text="*{getStatus() ? getStatus().getDisplay() : ''}">Status</td>
<td th:text="*{getClass() ? getClass().get(0).getDisplay() : ''}">Class</td>
<td th:text="*{getType() ? getType().get(0).getDisplay() : ''}">Type</td>
<th:block th:if="*{hasPriority()}">
<td th:text="*{getPriority() ? getPriority().get(0).getDisplay() : ''}">Priority</td>
</th:block>
<th:block th:unless="*{hasPriority()}">
<td></td>
</th:block>
<th:block th:if="*{hasPeriod()}">
<td th:text="*{#strings.concatReplaceNulls('', getPeriod().getStartElement().getValueAsString(), ' - ', getPeriod().getEndElement().getValueAsString())}">Period</td>
</th:block>
<th:block th:unless="*{hasPeriod()}">
<td></td>
</th:block>
<th:block th:if="*{hasLocation()}">
<th:block th:with="location = ${#fhirpath.evaluateFirst(#object, 'location[0].location.resolve()')}">
<th:block th:if="${location != null}">
<td th:text="~{location ? getName() : ''}">Location</td>
</th:block>
<th:block th:unless="${location != null}">
<td></td>
</th:block>
</th:block>
</th:block>
<th:block th:unless="*{hasLocation()}">
<td></td>
</th:block>
<th:block th:if="*{hasParticipant()}" th:fragment="renderParticipantRef(encounter)">
<td th:text="${#fhirpath.evaluate(#object, 'participant[0].individual.reference.resolve().name[0].family')[0]} + ', ' + ${#fhirpath.evaluate(#object, 'participant[0].individual.reference.resolve().name[0].given[0]')[0]}" >Participant</td>
</th:block>
<th:block th:unless="*{hasParticipant()}">
<td></td>
</th:block>
</tr>
</th:block>
</th:block>
</tbody>
</table>
</div>
You are about to leave the Smile Digital Health documentation and navigate to the Open Source HAPI-FHIR Documentation.