Skip to content

Commit

Permalink
Adds a z-score xpath calculation
Browse files Browse the repository at this point in the history
This makes using z-score much more flexible than the widget
implementation.

#4457
  • Loading branch information
garethbowen committed May 3, 2018
1 parent 472fbe5 commit 35e4195
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 219 deletions.
6 changes: 5 additions & 1 deletion static/js/enketo/OpenrosaXpathEvaluatorBinding.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
var _ = require('underscore');
var ExtendedXpathEvaluator = require('extended-xpath');
var openrosa_xpath_extensions = require('openrosa-xpath-extensions');
var medicExtensions = require('./medic-xpath-extensions');
var translator = require('./translator');

module.exports = function() {
Expand All @@ -12,6 +14,8 @@ module.exports = function() {
return evaluator.createNSResolver.apply( evaluator, arguments );
};
this.xml.jsEvaluate = function(e, contextPath, namespaceResolver, resultType, result) {
var extensions = openrosa_xpath_extensions(translator.t);
extensions.func = _.extend(extensions.func, medicExtensions.func);
var evaluator = new ExtendedXpathEvaluator(
function wrappedXpathEvaluator(v) {
// Node requests (i.e. result types greater than 3 (BOOLEAN)
Expand All @@ -23,7 +27,7 @@ module.exports = function() {
var doc = contextPath.ownerDocument;
return doc.evaluate(v, contextPath, namespaceResolver, wrappedResultType, result);
},
openrosa_xpath_extensions(translator.t));
extensions);
return evaluator.evaluate(e, contextPath, namespaceResolver, resultType, result);
};
window.JsXPathException =
Expand Down
16 changes: 16 additions & 0 deletions static/js/enketo/medic-xpath-extensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
var zscoreUtil;

module.exports = {
init: function(_zscoreUtil) {
zscoreUtil = _zscoreUtil;
},
func: {
'z-score': function(chartId, sex, x, y) {
var result = zscoreUtil(chartId, sex, x, y);
if (!result) {
return { t: 'str', v: '' };
}
return { t: 'num', v: result };
}
}
};
42 changes: 21 additions & 21 deletions static/js/enketo/widgets/z-score.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,27 +48,27 @@ define( function( require, exports, module ) {
};

ZScoreWidget.prototype._update = function(self) {
var options = {
sex: self.group.find( '.or-appearance-zscore-sex [data-checked=true] input' ).val(),
age: self.group.find( '.or-appearance-zscore-age input' ).val(),
weight: self.group.find( '.or-appearance-zscore-weight input' ).val(),
height: self.group.find( '.or-appearance-zscore-height input' ).val()
};
self.zScoreService(options)
.then(function(scores) {
self.group.find('.or-appearance-zscore-weight-for-age input')
.val(self._round(scores.weightForAge))
.trigger( 'change' );
self.group.find('.or-appearance-zscore-height-for-age input')
.val(self._round(scores.heightForAge))
.trigger( 'change' );
self.group.find('.or-appearance-zscore-weight-for-height input')
.val(self._round(scores.weightForHeight))
.trigger( 'change' );
})
.catch(function(err) {
self.logService.error('Error calculating z-score', err);
});
var sex = self.group.find( '.or-appearance-zscore-sex [data-checked=true] input' ).val();
var age = self.group.find( '.or-appearance-zscore-age input' ).val();
var weight = self.group.find( '.or-appearance-zscore-weight input' ).val();
var height = self.group.find( '.or-appearance-zscore-height input' ).val();
self.zScoreService().then(function(util) {
var wfa = util('weight-for-age', sex, age, weight);
self.group.find('.or-appearance-zscore-weight-for-age input')
.val(self._round(wfa))
.trigger( 'change' );
var hfa = util('height-for-age', sex, age, height);
self.group.find('.or-appearance-zscore-height-for-age input')
.val(self._round(hfa))
.trigger( 'change' );
var wfh = util('weight-for-height', sex, height, weight);
self.group.find('.or-appearance-zscore-weight-for-height input')
.val(self._round(wfh))
.trigger( 'change' );
})
.catch(function(err) {
self.logService.error('Error calculating z-score', err);
});
};

ZScoreWidget.prototype.destroy = function( /* element */ ) {};
Expand Down
30 changes: 21 additions & 9 deletions static/js/services/enketo.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var uuid = require('uuid/v4'),
pojo2xml = require('pojo2xml'),
xpathPath = require('../modules/xpath-element-path');
xpathPath = require('../modules/xpath-element-path'),
medicXpathExtensions = require('../enketo/medic-xpath-extensions');

/* globals EnketoForm */
angular.module('inboxServices').service('Enketo',
Expand All @@ -22,7 +23,8 @@ angular.module('inboxServices').service('Enketo',
TranslateFrom,
UserContact,
XmlForm,
XSLT
XSLT,
ZScore
) {
'use strict';
'ngInject';
Expand All @@ -32,6 +34,17 @@ angular.module('inboxServices').service('Enketo',
var FORM_ATTACHMENT_NAME = 'xml';
var REPORT_ATTACHMENT_NAME = this.REPORT_ATTACHMENT_NAME = 'content';

var init = function() {
ZScore()
.then(function(zscoreUtil) {
medicXpathExtensions.init(zscoreUtil);
})
.catch(function(err) {
$log.error('Error initialising zscore util', err);
});
};
var inited = init();

var replaceJavarosaMediaWithLoaders = function(id, form) {
form.find('img,video,audio').each(function() {
var elem = $(this);
Expand Down Expand Up @@ -324,18 +337,17 @@ angular.module('inboxServices').service('Enketo',
};

this.render = function(selector, id, instanceData, editedListener) {
return getUserContact()
.then(function() {
return renderForm(selector, id, instanceData, editedListener);
});
return $q.all([inited, getUserContact()]).then(function() {
return renderForm(selector, id, instanceData, editedListener);
});
};

this.renderContactForm = renderForm;

this.renderFromXmlString = function(selector, xmlString, instanceData, editedListener) {
return Language()
.then(function(language) {
return translateXml(xmlString, language);
return $q.all([inited, Language()])
.then(function(results) {
return translateXml(xmlString, results[1]);
})
.then(transformXml)
.then(function(doc) {
Expand Down
110 changes: 53 additions & 57 deletions static/js/services/z-score.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
angular.module('inboxServices').factory('ZScore',
function(
$log,
Changes,
DB
) {

Expand All @@ -9,44 +11,17 @@ angular.module('inboxServices').factory('ZScore',
var CONFIGURATION_DOC_ID = 'zscore-charts';
var MINIMUM_Z_SCORE = -4;
var MAXIMUM_Z_SCORE = 4;
var CONFIGURATIONS = [
{
id: 'weight-for-age',
property: 'weightForAge',
required: [ 'sex', 'age', 'weight' ],
xAxis: 'age',
yAxis: 'weight'
},
{
id: 'height-for-age',
property: 'heightForAge',
required: [ 'sex', 'age', 'height' ],
xAxis: 'age',
yAxis: 'height'
},
{
id: 'weight-for-height',
property: 'weightForHeight',
required: [ 'sex', 'height', 'weight' ],
xAxis: 'height',
yAxis: 'weight'
}
];

var findChart = function(charts, id) {
for (var i = 0; i < charts.length; i++) {
if (charts[i].id === id) {
return charts[i];
var tables;

var findTable = function(id) {
for (var i = 0; i < tables.length; i++) {
if (tables[i].id === id) {
return tables[i];
}
}
};

var hasRequiredOptions = function(configuration, options) {
return configuration.required.every(function(required) {
return !!options[required];
});
};

var findClosestDataSet = function(data, key) {
if (key < data[0].key || key > data[data.length - 1].key) {
// the key isn't covered by the configured data points
Expand Down Expand Up @@ -85,45 +60,66 @@ angular.module('inboxServices').factory('ZScore',
return lowerIndex + MINIMUM_Z_SCORE + ratio;
};

var calculate = function(configuration, charts, options) {
if (!hasRequiredOptions(configuration, options)) {
return;
}
var chart = findChart(charts, configuration.id);
if (!chart) {
// no chart configured in the database
return;
}
var sexData = chart.data[options.sex];
if (!sexData) {
// no data for the given sex
return;
}
var xAxisData = findClosestDataSet(sexData, options[configuration.xAxis]);
var calculate = function(data, x, y) {
var xAxisData = findClosestDataSet(data, x);
if (!xAxisData) {
// the key lies outside of the lookup table range
return;
}
return findZScore(xAxisData, options[configuration.yAxis]);
return findZScore(xAxisData, y);
};

return function(options) {
options = options || {};
var init = function() {
return DB().get(CONFIGURATION_DOC_ID)
.then(function(doc) {
var result = {};
CONFIGURATIONS.forEach(function(configuration) {
result[configuration.property] = calculate(configuration, doc.charts, options);
});
return result;
tables = doc.charts;
})
.catch(function(err) {
if (err.status === 404) {
throw new Error('zscore-charts doc not found');
return;
}
throw err;
});
};

Changes({
key: 'zscore-service',
filter: function(change) {
return change.id === CONFIGURATION_DOC_ID;
},
callback: function(change) {
tables = change.doc && change.doc.charts;
}
});

return function() {
return init().then(function() {
return function(tableId, sex, x, y) {
if (!tables) {
// log an error if the z-score utility is used but not configured
$log.error('Doc "' + CONFIGURATION_DOC_ID + '" not found');
return;
}
if (!sex || !x || !y) {
// the form may not have been filled out yet
return;
}
var table = findTable(tableId);
if (!table) {
// log an error if the z-score utility is used but not configured
$log.error('Requested z-score table not found', tableId);
return;
}
var data = table.data[sex];
if (!data) {
$log.error('The ' + tableId + ' z-score table is not configured for ' + sex + ' children');
// no data for the given sex
return;
}
return calculate(data, x, y);
};
});
};
}

);
1 change: 1 addition & 0 deletions tests/karma/unit/controllers/inbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ describe('InboxCtrl controller', () => {
$provide.value('UserSettings', sinon.stub());
$provide.value('Tour', { getTours: () => Promise.resolve([]) });
$provide.value('RulesEngine', { init: KarmaUtils.nullPromise()() });
$provide.value('Enketo', sinon.stub());
$provide.constant('APP_CONFIG', {
name: 'name',
version: 'version'
Expand Down
1 change: 1 addition & 0 deletions tests/karma/unit/services/enketo.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ describe('Enketo service', function() {
$provide.value('EnketoPrepopulationData', EnketoPrepopulationData);
$provide.value('AddAttachment', AddAttachment);
$provide.value('XmlForm', XmlForm);
$provide.value('ZScore', () => Promise.resolve(sinon.stub()));
$provide.value('$q', Q); // bypass $q so we don't have to digest
});
inject(function(_Enketo_) {
Expand Down
Loading

0 comments on commit 35e4195

Please sign in to comment.