Repository navigation
Twig Calculations
The following page shows some example calculations code for computing scores from the answers provided by the questionniare. Note that these scores should not be used for analysis as sometimes complex missing value checks are required which may not be performed by the module.
These calculations are done by the "Computed Twig" field in Drupal Webform. A nice video tutorial on the use of these computed elements is available here
The computed twig examples shown in the following sections are learnt from this very useful page on computed twig for calculation page on Drupal.
-
To add the value of the field in the calculation use the data.xxx notation where data. means the data of the xxx field. Please remember that xxx will be the KEY of the questionnaire element (field) and not the title. In practice this means for a question which reads as "What is the level of pain you had in the past 1 week" and the KEY "pain_q1" the value will be obtained when you use the variable data.pain_q1. This is an important gotcha and the reason why naming conventions are important for the questionnaire keys.
-
Curly brackets are used to enclose the value in two ways. This is important to remember (see this page for details):
-
{% ... %}will execute statements. Note the % signs inside the curly brackets. -
{{ .... }}will output the result of a calculation. Note the double curly brackers. - set allows us to set a value to a variable.
- if, else, elseif allow creating if else loops, always to be ended with
{% endif %}statement. -
{% for -%}loops allow us to iterate through values in an array. Note the - in front of the % sign. This is needed. To be ended by an{% endfor %}statement.
- The filter function filters the elements of an array based on criteria. For example
{% for v in scores|filter(v => v is not empty) -%}in this for loop, the filter is filtering out elements in the array scores which values are empty.
One of the issues with PRO questionnaires is that questions may not be answered. In this case transformed scores are harder to calculate. Simply putting a twig formuala which does sums will result in errors as missing values will not be computed. Following is a snippet of a twig calculation which calculates the average based on the questions that have been answered for the Xeqls physical functioning domain.
{% set scores = [data.xeql1,data.xeql6,data.xeql10,data.xeql12] %} {# Create an array called scores with values of 4 questions from which the physical functioning score will be derived.#}
{% set full_scores = [] %} {# Initialise an empty array called full_scores which will contain only non-empty values #}
{% for v in scores|filter(v => v is not empty) -%} {# Filter values from the scores array which are not empty using a for loop #}
{% set full_scores = full_scores|merge([v]) %} {# Put these values in the full_scores array #}
{% endfor %} {# End the for loop #}
{% if full_scores|length > 0 %} {# Check if the array is empty ie. none of the questions have been answered. Otherwise a divide by 0 error will occur when calculating average #}
{% set score = full_scores|reduce((carry,v) => carry+v) %} {# Obtain the sum of the full_scores array#}
{% set score = score/(full_scores|length) %} {# Divide by the length of the full_scores array to get an average value.#}
{{score}} {# Return the score value #}
{% else %}
{% set score = '' %} {# Make score is empty if none of the questions have been answered #}
{{ score }} {# Return the score value #}
{% endif %} {# End the loop #}
Example for CX24 Symptom Experience Score.
{# Set an item range for the answers as per EORTC. For 4 point liekert responses item range is 3 (4 - 1 = 3). For 6 point this will be 5. #}
{% set item_range = 3 %}
{# Initialize an array with scores from the questionnaires #}
{% set scores = [data.cx24_q31, data.cx24_q32, data.cx24_q33, data.cx24_q34, data.cx24_q35, data.cx24_q36, data.cx24_q37, data.cx24_q39, data.cx24_q41, data.cx24_q42, data.cx24_q43] %}
{# Filter out values which were not answered #}
{% set scores = scores | filter(v => v is not empty) %}
{# Obtain the number of responses to the questions #}
{% set answered = scores|length %}
{# This if statement prevents divison by 0 error if none of the questions were answered #}
{% if answered > 0 %}
{# Calculate sum of the values in the array #}
{% set raw_score = scores|reduce((carry,v) => carry+v) %}
{# Calculate the average raw score by dividing by the number of questions answered #}
{% set raw_score = raw_score/answered %}
{# Scale the raw score as per the EORTC rules. Note that the formula differs from symptom and functional scores #}
{% set score = ((raw_score-1)/item_range)*100 %}
{# round of value to 2 digits #}
{% set score = score|number_format(2, '.', ',')%}
{# start the else statement for empty array #}
{% else %}
{# Ensure that value is set to a empty value if none of the questions were answered #}
{% set score = '' %}
{# End the if else statement #}
{% endif %}
{# Display the resultant calculated score rounded off to 2 digits#}
{{score}}
- In the first line we define the item range for the scale. For scales where questions have a liekert response ranging between 1 to 4 the range is 3 (4 - 1 = 3).
- The second line of the statement is setting up an array called
scores. - We then remove the values in the array which are empty i.e. not answered.
- We calculate the number of items in the resulting array.
- Once this new
raw_scorearray is created we will calculate the average of thescoresto get theraw_score. Note that this calculation will only be done if the number of values in thefull_scoresarray is more than 0 (i.e. at least one value is present). Else it will throw a divide by 0 error. - Next the
raw scoreis transformed into ascoreranging between 0 - 100. Basically it is((raw_score-1)/range)*100where range is the difference between the highest and lowest values in the response option as this is a symptom scale. Calculation for functional scales will be different as indicated below. In these two questions, for example the range is 3 as the highest value for the response option is 4 and lowest is 1. - Finally we have a condition that if there are no items in the
scoresarray i.e. all answers are missing then thescorewill be blank.
The diagram of the code is shown below:
flowchart TD
A[Create Array of Values from questions to be included in score] --> B(Remove Blank Values from Array)
B --> C{Check Number of Elements in Array}
C -->|Zero Elements| D[Return Blank Score]
C -->|More than Zero| E[Calculate Sum of the array]
E --> F[Raw score = sum/number of elements in array i.e. average]
F -->|Linear Transformation| G[Score = Standardized Score ranging between 0 to 100]
To understand the logic for calculation of the average (raw score) from the available answers for a multi-item domain score please refer to the EORTC QLQ C30 scoring manual. As per the manual this is a simple form of imputation. Please see the screenshot below of page 10 of the scoring manual to understand this.

In the following section we show the calculation for the EORTC QLQ C30 Role functioning score.
{% set scores = [data.cx24_q31, data.cx24_q32, data.cx24_q33, data.cx24_q34, data.cx24_q35, data.cx24_q36, data.cx24_q37, data.cx24_q39, data.cx24_q41, data.cx24_q42, data.cx24_q43] %}
{% set full_scores = [] %}
{% for v in scores|filter(v => v is not empty) -%}
{% set full_scores = full_scores|merge([v]) %}
{% endfor %}
{% if full_scores|length > 0 %}
{% set raw_score = full_scores|reduce((carry,v) => carry+v) %}
{% set raw_score = raw_score/(full_scores|length) %}
{% set score = ((1-(raw_score-1)/3))*100 %}
{{score}}
{% else %}
{% set score = '' %}
{{ score }}
{% endif %}
The logic is self explanatory and as compared to the symptom scale we basically reverse the score by subtracting from 1.
PROCTCAE composite grades are calculated using a very different manner. Essentially grade scores are assigned based on the frequency, severity and interference items. The scoring algorithm is described by Basch et al.. As this is not a computation but more of a lookup of a value an array based lookup can be done. Note that this would NOT be an efficient way to implement this in a large installation where several hundreds of patients are filling PROCTCAE questionnaires. In that case a custom module would be a more efficient solution. However for our purposes the following computed twig code will lookup the composite grade. In the example below, we will show the composite grade for the Pain in the Abdominal Area PROCTCAE term.
{% set val1 = data.proctcae_abdominal_pain_frequency %}
{% set val2 = data.proctcae_abdominal_pain_severity %}
{% set val3 = data.proctcae_abdominal_pain_interference %}
{% set tbl = [
{'frequency': 0, 'severity': 0, 'interference': 0, 'grade': 0},
{'frequency': 1, 'severity': 0, 'interference': 0, 'grade': 0},
{'frequency': 1, 'severity': 0, 'interference': 1, 'grade': 1},
{'frequency': 1, 'severity': 0, 'interference': 2, 'grade': 1},
{'frequency': 1, 'severity': 1, 'interference': 0, 'grade': 1},
{'frequency': 1, 'severity': 1, 'interference': 1, 'grade': 1},
{'frequency': 1, 'severity': 1, 'interference': 2, 'grade': 1},
{'frequency': 1, 'severity': 2, 'interference': 0, 'grade': 1},
{'frequency': 1, 'severity': 0, 'interference': 3, 'grade': 2},
{'frequency': 1, 'severity': 0, 'interference': 4, 'grade': 2},
{'frequency': 1, 'severity': 1, 'interference': 3, 'grade': 2},
{'frequency': 1, 'severity': 1, 'interference': 4, 'grade': 2},
{'frequency': 1, 'severity': 2, 'interference': 1, 'grade': 2},
{'frequency': 1, 'severity': 2, 'interference': 2, 'grade': 2},
{'frequency': 1, 'severity': 2, 'interference': 3, 'grade': 2},
{'frequency': 1, 'severity': 3, 'interference': 0, 'grade': 2},
{'frequency': 1, 'severity': 3, 'interference': 1, 'grade': 2},
{'frequency': 1, 'severity': 3, 'interference': 2, 'grade': 2},
{'frequency': 1, 'severity': 4, 'interference': 0, 'grade': 2},
{'frequency': 1, 'severity': 4, 'interference': 1, 'grade': 2},
{'frequency': 1, 'severity': 2, 'interference': 4, 'grade': 3},
{'frequency': 1, 'severity': 3, 'interference': 3, 'grade': 3},
{'frequency': 1, 'severity': 3, 'interference': 4, 'grade': 3},
{'frequency': 1, 'severity': 4, 'interference': 2, 'grade': 3},
{'frequency': 1, 'severity': 4, 'interference': 3, 'grade': 3},
{'frequency': 1, 'severity': 4, 'interference': 4, 'grade': 3},
{'frequency': 2, 'severity': 0, 'interference': 0, 'grade': 0},
{'frequency': 2, 'severity': 0, 'interference': 1, 'grade': 1},
{'frequency': 2, 'severity': 0, 'interference': 2, 'grade': 1},
{'frequency': 2, 'severity': 1, 'interference': 0, 'grade': 1},
{'frequency': 2, 'severity': 1, 'interference': 1, 'grade': 1},
{'frequency': 2, 'severity': 1, 'interference': 2, 'grade': 1},
{'frequency': 2, 'severity': 0, 'interference': 3, 'grade': 2},
{'frequency': 2, 'severity': 0, 'interference': 4, 'grade': 2},
{'frequency': 2, 'severity': 1, 'interference': 3, 'grade': 2},
{'frequency': 2, 'severity': 1, 'interference': 4, 'grade': 2},
{'frequency': 2, 'severity': 2, 'interference': 0, 'grade': 2},
{'frequency': 2, 'severity': 2, 'interference': 1, 'grade': 2},
{'frequency': 2, 'severity': 2, 'interference': 2, 'grade': 2},
{'frequency': 2, 'severity': 3, 'interference': 0, 'grade': 2},
{'frequency': 2, 'severity': 3, 'interference': 1, 'grade': 2},
{'frequency': 2, 'severity': 3, 'interference': 2, 'grade': 2},
{'frequency': 2, 'severity': 4, 'interference': 0, 'grade': 2},
{'frequency': 2, 'severity': 4, 'interference': 1, 'grade': 2},
{'frequency': 2, 'severity': 2, 'interference': 3, 'grade': 3},
{'frequency': 2, 'severity': 2, 'interference': 4, 'grade': 3},
{'frequency': 2, 'severity': 3, 'interference': 3, 'grade': 3},
{'frequency': 2, 'severity': 3, 'interference': 4, 'grade': 3},
{'frequency': 2, 'severity': 4, 'interference': 2, 'grade': 3},
{'frequency': 2, 'severity': 4, 'interference': 3, 'grade': 3},
{'frequency': 2, 'severity': 4, 'interference': 4, 'grade': 3},
{'frequency': 3, 'severity': 0, 'interference': 0, 'grade': 1},
{'frequency': 3, 'severity': 0, 'interference': 1, 'grade': 1},
{'frequency': 3, 'severity': 0, 'interference': 2, 'grade': 1},
{'frequency': 3, 'severity': 1, 'interference': 0, 'grade': 1},
{'frequency': 3, 'severity': 1, 'interference': 1, 'grade': 1},
{'frequency': 3, 'severity': 1, 'interference': 2, 'grade': 1},
{'frequency': 3, 'severity': 0, 'interference': 3, 'grade': 2},
{'frequency': 3, 'severity': 0, 'interference': 4, 'grade': 2},
{'frequency': 3, 'severity': 1, 'interference': 3, 'grade': 2},
{'frequency': 3, 'severity': 1, 'interference': 4, 'grade': 2},
{'frequency': 3, 'severity': 2, 'interference': 0, 'grade': 2},
{'frequency': 3, 'severity': 2, 'interference': 1, 'grade': 2},
{'frequency': 3, 'severity': 2, 'interference': 2, 'grade': 2},
{'frequency': 3, 'severity': 3, 'interference': 0, 'grade': 2},
{'frequency': 3, 'severity': 3, 'interference': 1, 'grade': 2},
{'frequency': 3, 'severity': 4, 'interference': 0, 'grade': 2},
{'frequency': 3, 'severity': 4, 'interference': 1, 'grade': 2},
{'frequency': 3, 'severity': 2, 'interference': 3, 'grade': 3},
{'frequency': 3, 'severity': 2, 'interference': 4, 'grade': 3},
{'frequency': 3, 'severity': 3, 'interference': 2, 'grade': 3},
{'frequency': 3, 'severity': 3, 'interference': 3, 'grade': 3},
{'frequency': 3, 'severity': 3, 'interference': 4, 'grade': 3},
{'frequency': 3, 'severity': 4, 'interference': 2, 'grade': 3},
{'frequency': 3, 'severity': 4, 'interference': 3, 'grade': 3},
{'frequency': 3, 'severity': 4, 'interference': 4, 'grade': 3},
{'frequency': 4, 'severity': 0, 'interference': 0, 'grade': 1},
{'frequency': 4, 'severity': 0, 'interference': 1, 'grade': 1},
{'frequency': 4, 'severity': 0, 'interference': 2, 'grade': 1},
{'frequency': 4, 'severity': 1, 'interference': 0, 'grade': 1},
{'frequency': 4, 'severity': 1, 'interference': 1, 'grade': 1},
{'frequency': 4, 'severity': 0, 'interference': 3, 'grade': 2},
{'frequency': 4, 'severity': 0, 'interference': 4, 'grade': 2},
{'frequency': 4, 'severity': 1, 'interference': 2, 'grade': 2},
{'frequency': 4, 'severity': 1, 'interference': 3, 'grade': 2},
{'frequency': 4, 'severity': 2, 'interference': 0, 'grade': 2},
{'frequency': 4, 'severity': 2, 'interference': 1, 'grade': 2},
{'frequency': 4, 'severity': 2, 'interference': 2, 'grade': 2},
{'frequency': 4, 'severity': 3, 'interference': 0, 'grade': 2},
{'frequency': 4, 'severity': 3, 'interference': 1, 'grade': 2},
{'frequency': 4, 'severity': 4, 'interference': 0, 'grade': 2},
{'frequency': 4, 'severity': 4, 'interference': 1, 'grade': 2},
{'frequency': 4, 'severity': 1, 'interference': 4, 'grade': 3},
{'frequency': 4, 'severity': 2, 'interference': 3, 'grade': 3},
{'frequency': 4, 'severity': 2, 'interference': 4, 'grade': 3},
{'frequency': 4, 'severity': 3, 'interference': 2, 'grade': 3},
{'frequency': 4, 'severity': 3, 'interference': 3, 'grade': 3},
{'frequency': 4, 'severity': 3, 'interference': 4, 'grade': 3},
{'frequency': 4, 'severity': 4, 'interference': 2, 'grade': 3},
{'frequency': 4, 'severity': 4, 'interference': 3, 'grade': 3},
{'frequency': 4, 'severity': 4, 'interference': 4, 'grade': 3},
] %}
{% set matchingRow = tbl|filter(item => item.frequency == val1 and item.severity == val2 and item.interference == val3)|last %}
{% if matchingRow %}
{% set grade = matchingRow.grade %}
{% elseif val1 is not empty %}
{% set grade = 0 %}
{% else %}
{% set grade = '' %}
{% endif %}
{{grade}}
- To walk through the code. The first set of instructions assigns the frequency, severity and interference scores to the val1, val2 and val3 objects. Note that choice of the name is deliberate to reduce the need to modify the last bit of the ifelse loop in the code for other calculation scenarios (see below for example).
- Next a lookup array is initialised with values corresponding to Basch et al. (see Appendix for the values).
- Finally, we create a matchingRow array which has the last value from the lookup table (called tbl).
- We pass through a if else statement which checks if the matchingRow array exists and if does then sets the grade to the value obtained from the grade row in the lookup table. if not it checks if the frequency value is filled. Note that this is a start of the branching logic for display of the two subsequent questions in PROCTCAE. Hence if this is 0 grade is set at 0. Finally if both conditions are not met, then it returns a blank value.
{% set val1 = data.proctcae_palpitation_frequency %}
{% set val2 = data.proctcae_palpitation_severity %}
{% set tbl = [
{ 'frequency' : 0, 'severity': 0, 'grade' : 0},
{ 'frequency' : 1, 'severity': 0, 'grade' : 1},
{ 'frequency' : 1, 'severity': 1, 'grade' : 1},
{ 'frequency' : 1, 'severity': 2, 'grade' : 1},
{ 'frequency' : 2, 'severity': 0, 'grade' : 1},
{ 'frequency' : 2, 'severity': 1, 'grade' : 1},
{ 'frequency' : 3, 'severity': 0, 'grade' : 1},
{ 'frequency' : 3, 'severity': 1, 'grade' : 1},
{ 'frequency' : 4, 'severity': 0, 'grade' : 1},
{ 'frequency' : 4, 'severity': 1, 'grade' : 1},
{ 'frequency' : 1, 'severity': 3, 'grade' : 2},
{ 'frequency' : 1, 'severity': 4, 'grade' : 2},
{ 'frequency' : 2, 'severity': 2, 'grade' : 2},
{ 'frequency' : 2, 'severity': 3, 'grade' : 2},
{ 'frequency' : 2, 'severity': 4, 'grade' : 2},
{ 'frequency' : 3, 'severity': 2, 'grade' : 2},
{ 'frequency' : 4, 'severity': 2, 'grade' : 2},
{ 'frequency' : 3, 'severity': 3, 'grade' : 3},
{ 'frequency' : 3, 'severity': 4, 'grade' : 3},
{ 'frequency' : 4, 'severity': 3, 'grade' : 3},
{ 'frequency' : 4, 'severity': 4, 'grade' : 3},
] %}
{% set matchingRow = tbl|filter(item => item.frequency == val1 and item.severity == val2)|last %}
{% if matchingRow %}
{% set grade = matchingRow.grade %}
{% elseif val1 is not empty %}
{% set grade = 0 %}
{% else %}
{% set grade = '' %}
{% endif %}
{{grade}}
{% set val1 = data.proctcae_dyspnea_severity %}
{% set val2 = data.proctcae_dyspnea_interference %}
{% set tbl = [
{ 'severity' : 0, 'interference': 0, 'grade' : 0},
{ 'severity' : 1, 'interference': 0, 'grade' : 1},
{ 'severity' : 1, 'interference': 1, 'grade' : 1},
{ 'severity' : 1, 'interference': 2, 'grade' : 1},
{ 'severity' : 2, 'interference': 0, 'grade' : 1},
{ 'severity' : 2, 'interference': 1, 'grade' : 1},
{ 'severity' : 3, 'interference': 0, 'grade' : 1},
{ 'severity' : 1, 'interference': 3, 'grade' : 2},
{ 'severity' : 1, 'interference': 4, 'grade' : 2},
{ 'severity' : 2, 'interference': 2, 'grade' : 2},
{ 'severity' : 2, 'interference': 3, 'grade' : 2},
{ 'severity' : 3, 'interference': 1, 'grade' : 2},
{ 'severity' : 3, 'interference': 2, 'grade' : 2},
{ 'severity' : 4, 'interference': 0, 'grade' : 2},
{ 'severity' : 4, 'interference': 1, 'grade' : 2},
{ 'severity' : 4, 'interference': 2, 'grade' : 2},
{ 'severity' : 2, 'interference': 4, 'grade' : 3},
{ 'severity' : 3, 'interference': 3, 'grade' : 3},
{ 'severity' : 3, 'interference': 4, 'grade' : 3},
{ 'severity' : 4, 'interference': 3, 'grade' : 3},
{ 'severity' : 4, 'interference': 4, 'grade' : 3},
] %}
{% set matchingRow = tbl|filter(item => item.severity == val1 and item.interference == val2)|last %}
{% if matchingRow %}
{% set grade = matchingRow.grade %}
{% elseif val1 is not empty %}
{% set grade = 0 %}
{% else %}
{% set grade = '' %}
{% endif %}
{{grade}}
{% set val1 = data.proctcae_fecal_incontinence_frequency %}
{% set val2 = data.proctcae_fecal_incontinence_interference %}
{% set tbl = [
{ 'frequency' : 0, 'interference': 0, 'grade' : 0},
{ 'frequency' : 1, 'interference': 0, 'grade' : 1},
{ 'frequency' : 1, 'interference': 1, 'grade' : 1},
{ 'frequency' : 1, 'interference': 2, 'grade' : 1},
{ 'frequency' : 2, 'interference': 0, 'grade' : 1},
{ 'frequency' : 2, 'interference': 1, 'grade' : 1},
{ 'frequency' : 2, 'interference': 2, 'grade' : 1},
{ 'frequency' : 3, 'interference': 0, 'grade' : 1},
{ 'frequency' : 3, 'interference': 1, 'grade' : 1},
{ 'frequency' : 4, 'interference': 0, 'grade' : 1},
{ 'frequency' : 4, 'interference': 1, 'grade' : 1},
{ 'frequency' : 1, 'interference': 3, 'grade' : 2},
{ 'frequency' : 1, 'interference': 4, 'grade' : 2},
{ 'frequency' : 2, 'interference': 3, 'grade' : 2},
{ 'frequency' : 2, 'interference': 4, 'grade' : 2},
{ 'frequency' : 3, 'interference': 2, 'grade' : 2},
{ 'frequency' : 4, 'interference': 2, 'grade' : 2},
{ 'frequency' : 3, 'interference': 3, 'grade' : 3},
{ 'frequency' : 3, 'interference': 4, 'grade' : 3},
{ 'frequency' : 4, 'interference': 3, 'grade' : 3},
{ 'frequency' : 4, 'interference': 4, 'grade' : 3},
] %}
{% set matchingRow = tbl|filter(item => item.frequency == val1 and item.interference == val2)|last %}
{% if matchingRow %}
{% set grade = matchingRow.grade %}
{% elseif val1 is not empty %}
{% set grade = 0 %}
{% else %}
{% set grade = '' %}
{% endif %}
{{grade}}
- Home
- Installation
- Enabling Modules
- Patient Access Configuration Logic
- Multilingual Capability
-
Configuration
- Create Roles
- Super Login Configuration
- Login Logout Redirect Per Role
- Account Setting Configuration
- Email Configuration
- Date Time Format Configuration
- Language Configuration
- Language Detection Configuration
- Entity Access by Role Field Configuration
- Permission Configuration
- Administer Users By Role Configuration
- Enabled Modules
- Pathauto Configuration
- Redirect Configuration
- Questionnaire Configuration
- Howtos