diff --git a/js/pages/cohort-definitions/cohort-definition-manager.html b/js/pages/cohort-definitions/cohort-definition-manager.html
index 2665a0ca7..9f81e0d29 100644
--- a/js/pages/cohort-definitions/cohort-definition-manager.html
+++ b/js/pages/cohort-definitions/cohort-definition-manager.html
@@ -330,7 +330,7 @@
+
@@ -793,4 +793,13 @@
+
+
+
+
+
diff --git a/js/pages/cohort-definitions/cohort-definition-manager.js b/js/pages/cohort-definitions/cohort-definition-manager.js
index 99f13fdc5..098921f8a 100644
--- a/js/pages/cohort-definitions/cohort-definition-manager.js
+++ b/js/pages/cohort-definitions/cohort-definition-manager.js
@@ -58,7 +58,8 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
'utilities/sql',
'components/conceptset/conceptset-list',
'components/name-validation',
- 'components/versions/versions'
+ 'components/versions/versions',
+ 'databindings/tooltipBinding'
], function (
$,
ko,
@@ -601,6 +602,16 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
title: ko.i18n('cohortDefinitions.cohortDefinitionManager.panels.generationDuration', 'Generation Duration'),
data: 'executionDuration'
}, {
+ title: ko.i18n(
+ 'cohortDefinitions.cohortDefinitionManager.panels.generationDuration3',
+ 'Demographics'
+ ),
+ data: 'viewDemographic',
+ sortable: false,
+ tooltip: 'Results with Demographics',
+ render: () =>
+ ``,
+ },{
sortable: false,
className: 'generation-buttons-column',
render: () => ``
@@ -653,6 +664,8 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
source.personCount(commaFormatted(info.personCount));
source.recordCount(commaFormatted(info.recordCount));
source.failMessage(info.failMessage);
+ source.ccGenerateId(info.ccGenerateId);
+ source.viewDemographic(info.isChooseDemographic);
}
}
});
@@ -1094,13 +1107,25 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
if (this.selectedSource() && this.selectedSource().sourceId === source.sourceId) {
this.toggleCohortReport(null);
}
- cohortDefinitionService.generate(this.currentCohortDefinition().id(), source.sourceKey)
+ cohortDefinitionService.generate(this.currentCohortDefinition().id(), source.sourceKey, source.viewDemographic())
.catch(this.authApi.handleAccessDenied)
.then(({data}) => {
jobDetailsService.createJob(data);
});
}
+ handleCheckboxDemographic(source) {
+ const targetSource = this.getSourceKeyInfo(source.sourceKey);
+ targetSource.viewDemographic(targetSource.viewDemographic());
+ const restSourceInfos = this.cohortDefinitionSourceInfo().filter(
+ (d) => {
+ return d.sourceKey !== source.sourceKey;
+ }
+ );
+
+ this.cohortDefinitionSourceInfo([...restSourceInfos, targetSource])
+ }
+
cancelGenerate (source) {
this.stopping()[source.sourceKey](true);
cohortDefinitionService.cancelGenerate(this.currentCohortDefinition().id(), source.sourceKey);
@@ -1261,6 +1286,9 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
}
cdsi.failMessage = ko.observable(sourceInfo.failMessage);
cdsi.createdBy = ko.observable(sourceInfo.createdBy);
+ cdsi.viewDemographic = ko.observable(sourceInfo?.viewDemographic || sourceInfo.isChooseDemographic || false);
+ cdsi.tooltipDemographic = ko.observable(sourceInfo?.tooltipDemographic || null);
+ cdsi.ccGenerateId = ko.observable(sourceInfo.ccGenerateId);
} else {
cdsi.isValid = ko.observable(false);
cdsi.isCanceled = ko.observable(false);
@@ -1271,6 +1299,9 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
cdsi.recordCount = ko.observable('n/a');
cdsi.failMessage = ko.observable(null);
cdsi.createdBy = ko.observable(null);
+ cdsi.viewDemographic = ko.observable(false);
+ cdsi.tooltipDemographic = ko.observable(null);
+ cdsi.ccGenerateId = ko.observable(null);
}
return cdsi;
}
@@ -1316,6 +1347,23 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
async prepareCohortDefinition(cohortDefinitionId, conceptSetId, selectedSourceId, sourceKey, versionNumber) {
this.isLoading(true);
+ ko.bindingHandlers.tooltip = {
+ init: function (element, valueAccessor) {
+ const value = ko.utils.unwrapObservable(valueAccessor());
+ $("[aria-label='Demographics']").attr('data-original-title', 'Results with Demographics').bstooltip({
+ html: true,
+ container:'body',
+ });
+ $(element).attr('data-original-title', value).bstooltip({
+ html: true,
+ container:'body'
+ });
+ },
+ update: function (element, valueAccessor) {
+ const value = ko.utils.unwrapObservable(valueAccessor());
+ $(element).attr('data-original-title', value);
+ }
+ }
if(parseInt(cohortDefinitionId) === 0) {
this.setNewCohortDefinition();
} else if (versionNumber) {
@@ -1348,6 +1396,49 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
}
}
+ addToolTipDemographic(source){
+ const targetSource = this.getSourceKeyInfo(source?.sourceKey);
+ targetSource?.tooltipDemographic('Results with Demographics');
+ const restSourceInfos = this.cohortDefinitionSourceInfo().filter(
+ (d) => {
+ return d.sourceKey !== source?.sourceKey;
+ }
+ );
+ this.cohortDefinitionSourceInfo([
+ ...restSourceInfos,
+ targetSource
+ ])
+ }
+
+ removeToolTipDemographic(source){
+ const targetSource = this.getSourceKeyInfo(source?.sourceKey);
+ targetSource?.tooltipDemographic(null);
+ const restSourceInfos = this.cohortDefinitionSourceInfo().filter(
+ (d) => {
+ return d?.sourceKey !== source?.sourceKey;
+ }
+ );
+ this.cohortDefinitionSourceInfo([
+ ...restSourceInfos,
+ targetSource
+ ])
+ }
+
+ handleViewDemographic(source) {
+ const targetSource = this.getSourceKeyInfo(source?.sourceKey);
+ targetSource.viewDemographic(!targetSource.viewDemographic());
+ targetSource?.tooltipDemographic(null);
+ const restSourceInfos = this.cohortDefinitionSourceInfo().filter(
+ (d) => {
+ return d?.sourceKey !== source?.sourceKey;
+ }
+ );
+ this.cohortDefinitionSourceInfo([
+ ...restSourceInfos,
+ targetSource
+ ])
+ }
+
checkifDataLoaded(cohortDefinitionId, conceptSetId, sourceKey) {
if (this.currentCohortDefinition() && this.currentCohortDefinition().id() == cohortDefinitionId) {
if (this.currentConceptSet() && this.currentConceptSet().id == conceptSetId) {
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/cohort-reports.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/cohort-reports.js
index 086027e64..157a24f8e 100644
--- a/js/pages/cohort-definitions/components/reporting/cohort-reports/cohort-reports.js
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/cohort-reports.js
@@ -21,7 +21,7 @@ define([
PluginRegistry.add(globalConstants.pluginTypes.COHORT_REPORT, {
title: ko.i18n('cohortDefinitions.cohortreports.inclusionReport', 'Inclusion Report'),
priority: 1,
- html: ``
+ html: ``
});
class CohortReports extends Component {
@@ -30,18 +30,37 @@ define([
this.sourceKey = ko.computed(() => params.source() && params.source().sourceKey);
this.cohortId = ko.computed(() => params.cohort().id());
-
+ this.isViewDemographic = ko.computed(() => params.source() && params.source().viewDemographic());
+ this.ccGenerateId = ko.computed(() => params.infoSelected() && params.infoSelected().ccGenerateId());
+
const componentParams = {
sourceKey: this.sourceKey,
- cohortId: this.cohortId
+ cohortId: this.cohortId,
+ isViewDemographic: this.isViewDemographic,
+ ccGenerateId: this.ccGenerateId,
};
this.tabs = PluginRegistry.findByType(globalConstants.pluginTypes.COHORT_REPORT).map(t => ({ ...t, componentParams }));
+
+ if (this.isViewDemographic()) {
+ this.tabs.push({
+ title: ko.i18n('cohortDefinitions.cohortreports.tabs.byPerson3', 'Demographics'),
+ componentName: 'demographic-report',
+ componentParams: {
+ ...componentParams,
+ reportType: constants.INCLUSION_REPORT.BY_DEMOGRAPHIC,
+ buttons: null,
+ tableDom: "Blfiprt"
+ }
+ });
+ }
}
dispose() {
this.sourceKey.dispose();
this.cohortId.dispose();
+ this.isViewDemographic.dispose();
+ this.ccGenerateId.dispose();
}
}
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/const.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/const.js
index 2154963de..b2332ef80 100644
--- a/js/pages/cohort-definitions/components/reporting/cohort-reports/const.js
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/const.js
@@ -2,10 +2,18 @@ define([], () => {
const INCLUSION_REPORT = {
BY_EVENT: 0,
BY_PERSON: 1,
+ BY_DEMOGRAPHIC: 2
+ };
+
+ const feAnalysisTypes = {
+ PRESET: 'PRESET',
+ CRITERIA_SET: 'CRITERIA_SET',
+ CUSTOM_FE: 'CUSTOM_FE'
};
return {
- INCLUSION_REPORT
+ INCLUSION_REPORT,
+ feAnalysisTypes
};
}
);
\ No newline at end of file
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/BaseDistributionStatConverter.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/BaseDistributionStatConverter.js
new file mode 100644
index 000000000..771fcbab6
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/BaseDistributionStatConverter.js
@@ -0,0 +1,86 @@
+define([
+ 'knockout',
+ './BaseStatConverter',
+ './DistributionStat',
+], function (
+ ko,
+ BaseStatConverter,
+ DistributionStat,
+) {
+
+ class BaseDistributionStatConverter extends BaseStatConverter {
+
+ constructor(classes) {
+ super(classes);
+ }
+
+ convertAnalysisToTabularData(analysis, stratas = null) {
+
+ const result = super.convertAnalysisToTabularData(analysis, stratas);
+ stratas && stratas.filter(s => result.data.findIndex(row => row.strataId === s.strataId) < 0)
+ .forEach(s => result.data.push(this.getResultObject(
+ {
+ analysisId: analysis.analysisId,
+ analysisName: analysis.analysisName,
+ domainId: analysis.domainId,
+ cohorts: [],
+ strataId: s.strataId,
+ strataName: s.strataName,
+ }
+ )));
+ return result;
+ }
+
+ getRowId(stat) {
+ return stat.strataId * 100000 + stat.covariateId;
+ }
+
+ getResultObject(stat) {
+ return new DistributionStat(stat);
+ }
+
+ extractStrata(stat) {
+ return { strataId: 0, strataName: '' };
+ }
+
+ getDefaultColumns(analysis) {
+ return [{
+ title: ko.i18n('columns.strata', 'Strata'),
+ data: 'strataName',
+ className: this.classes('col-distr-title'),
+ xssSafe:false,
+ },
+ {
+ title: 'Covariate',
+ data: 'covariateName',
+ className: this.classes('col-distr-cov'),
+ xssSafe: false,
+ },
+ {
+ title: 'Value field',
+ data: (row, type) => {
+ let data = (row.faType === 'CRITERIA_SET' && row.aggregateName) || "Events count" ;
+ if (row.missingMeansZero) {
+ data = data + "*";
+ }
+ return data;
+ }
+ }];
+ }
+
+ convertFields(result, strataId, cohortId, stat, prefix) {
+ result.strataName = stat.strataName;
+ ['count', 'avg', 'pct', 'stdDev', 'median', 'max', 'min', 'p10', 'p25', 'p75', 'p90'].forEach(field => {
+ const statName = prefix ? prefix + field.charAt(0).toUpperCase() + field.slice(1) : field;
+ this.setNestedValue(result, field, strataId, cohortId, stat[statName]);
+ });
+ }
+
+ convertCompareFields(result, strataId, stat) {
+ this.convertFields(result, strataId, stat.targetCohortId, stat, "target");
+ this.convertFields(result, strataId, stat.comparatorCohortId, stat, "comparator");
+ }
+ }
+
+ return BaseDistributionStatConverter;
+});
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/BaseStatConverter.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/BaseStatConverter.js
new file mode 100644
index 000000000..3ae1fdd12
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/BaseStatConverter.js
@@ -0,0 +1,165 @@
+define([
+ 'knockout',
+'numeral',
+'../utils',
+], function (
+ko,
+numeral,
+utils,
+) {
+
+
+class BaseStatConverter {
+
+ constructor(classes) {
+ this.classes = classes;
+ }
+
+ convertAnalysisToTabularData(analysis, stratas = null) {
+ let columns = this.getDefaultColumns(analysis);
+
+ const data = new Map();
+ const cohorts = Array.from(analysis.cohorts);
+ const strataNames = (stratas && stratas.reduce((map, s) => {
+ const { strataId, strataName } = this.extractStrata(s);
+ return map.set(strataId, strataName);
+ }, new Map())) || new Map();
+
+ let mapCovariate;
+ mapCovariate = (stat) => {
+ let row;
+ const rowId = this.getRowId(stat);
+ if (!data.has(rowId)) {
+ row = this.getResultObject({
+ analysisId: analysis.analysisId,
+ analysisName: analysis.analysisName,
+ domainId: analysis.domainId,
+ cohorts: cohorts || [],
+ ...stat
+ });
+
+ data.set(rowId, row);
+ } else {
+ row = data.get(rowId);
+ }
+
+ const { strataId, strataName } = this.extractStrata(stat);
+
+ if (!strataNames.has(strataId)) {
+ strataNames.set(strataId, strataName);
+ }
+
+ if (analysis.isComparative) {
+ row.stdDiff = this.formatStdDiff(stat.diff);
+ this.convertCompareFields(row, strataId, stat);
+ } else {
+ this.convertFields(row, strataId, stat.cohortId, stat);
+ }
+ };
+
+ analysis.items.forEach((c, i) => mapCovariate(c));
+
+ cohorts.forEach((c, i) => {
+ for (let strataId of utils.sortedStrataNames(strataNames).map(s => s.id)) {
+ columns = columns.concat(this.getReportColumns(strataId, c.cohortId));
+ }
+ });
+
+ const strataOnly = !strataNames.has(0) && data.size > 0;
+ const stratified = !(strataNames.has(0) && strataNames.size === 1) && data.size > 0;
+
+ if (!strataOnly && analysis.isComparative) {
+ columns.push(this.getStdDiffColumn());
+ }
+
+ return {
+ type: analysis.type,
+ domainIds: analysis.domainIds,
+ analysisName: analysis.analysisName,
+ analysisId: analysis.analysisId,
+ strataOnly,
+ stratified,
+ cohorts: cohorts,
+ isSummary: analysis.isSummary,
+ strataNames: strataNames,
+ defaultColNames: this.getDefaultColumns().map(col => col.title),
+ perStrataColNames: this.getReportColumns(0, 0).map(col => col.title),
+ columns: columns.map(col => ({...col, title: ko.unwrap(col.title)})),
+ defaultSort: this.getDefaultSort(columns.length, cohorts.length),
+ data: Array.from(data.values()),
+ };
+ }
+
+ getDefaultSort(columnsCount, cohortsCount) {
+ return [[columnsCount - 1, "desc"]];
+ }
+
+ getRowId(stat) {
+ throw "Override getRowId with actual implementation";
+ }
+
+ extractStrata(stat) {
+ throw "Override extractStrata with actual implementation";
+ }
+
+ setNestedValue(result, field, strataId, cohortId, value) {
+ if (result[field][strataId] === undefined) {
+ result[field][strataId] = {};
+ }
+ result[field][strataId][cohortId] = value;
+ }
+
+ getStdDiffColumn() {
+ return {
+ title: 'Std diff',
+ render: (s, p, d) => d.stdDiff,
+ className: this.classes('col-dist-std-diff'),
+ type: 'numberAbs'
+ };
+ }
+
+ formatStdDiff(val) {
+ if (+val == Infinity || +val == -Infinity) {
+ return "";
+ }
+ else {
+ return numeral(val).format('0,0.0000');
+ }
+ }
+
+ formatPct(val) {
+ return numeral(val).format('0.00') + '%';
+ }
+
+ getColumn(label, field, strata, cohortId, formatter) {
+ return {
+ title: label,
+ className: field === 'pct' ? 'pct-cell' : '',
+ render: (s, p, d) => {
+ let res = d[field][strata] && d[field][strata][cohortId] || 0;
+ if (p === "display" && formatter) {
+ res = formatter(res);
+ }
+ if (field === 'pct') {
+ return ``;
+ }
+ return res;
+ }
+ };
+ }
+
+ getCountColumn(label, field, strata, cohortId) {
+ return this.getColumn(label, field, strata, cohortId, v => numeral(v).format());
+ }
+
+ getDecimal2Column(label, field, strata, cohortId) {
+ return this.getColumn(label, field, strata, cohortId, v => numeral(v).format('0.00'));
+ }
+
+ getPctColumn(label, field, strata, cohortId) {
+ return this.getColumn(label, field, strata, cohortId, this.formatPct);
+ }
+}
+
+return BaseStatConverter;
+});
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/ComparativeDistributionStatConverter.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/ComparativeDistributionStatConverter.js
new file mode 100644
index 000000000..5558cba96
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/ComparativeDistributionStatConverter.js
@@ -0,0 +1,24 @@
+define([
+ 'knockout',
+ './BaseDistributionStatConverter',
+ './DistributionStat',
+], function (
+ ko,
+ BaseDistributionStatConverter,
+) {
+
+ class ComparativeDistributionStatConverter extends BaseDistributionStatConverter {
+
+ getReportColumns(strataId, cohortId) {
+ return [
+ this.getCountColumn(ko.i18n('columns.personsCount', 'Persons'), 'count', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.avg', 'Avg'), 'avg', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.stddev', 'Std Dev'), 'stdDev', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.median', 'Median'), 'median', strataId, cohortId),
+ ];
+ }
+
+ }
+
+ return ComparativeDistributionStatConverter;
+});
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/DistributionStat.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/DistributionStat.js
new file mode 100644
index 000000000..6a502f5f6
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/DistributionStat.js
@@ -0,0 +1,29 @@
+define([
+ './PrevalenceStat'
+], function (PrevalenceStat) {
+
+ class DistributionStat extends PrevalenceStat {
+
+ constructor(stat) {
+ super(stat);
+ this.strataId = stat.strataId;
+ this.strataName = stat.strataName;
+ this.covariateId = stat.covariateId;
+ this.covariateName = stat.covariateName;
+ this.aggregateId = stat.aggregateId;
+ this.aggregateName = stat.aggregateName;
+ this.missingMeansZero = stat.missingMeansZero;
+ this.avg = [];
+ this.stdDev = [];
+ this.median = [];
+ this.max = [];
+ this.min = [];
+ this.p10 = [];
+ this.p25 = [];
+ this.p75 = [];
+ this.p90 = [];
+ }
+ }
+
+ return DistributionStat;
+});
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/DistributionStatConverter.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/DistributionStatConverter.js
new file mode 100644
index 000000000..f67244c19
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/DistributionStatConverter.js
@@ -0,0 +1,33 @@
+define([
+ 'knockout',
+ './BaseDistributionStatConverter',
+ './DistributionStat',
+], function (
+ ko,
+ BaseDistributionStatConverter,
+) {
+
+ class DistributionStatConverter extends BaseDistributionStatConverter {
+
+ getReportColumns(strataId, cohortId) {
+ return [
+ this.getCountColumn(ko.i18n('columns.personsCount', 'Persons'), 'count', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.avg', 'Avg'), 'avg', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.stddev', 'Std Dev'), 'stdDev', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.min', 'Min'), 'min', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.p10', 'P10'), 'p10', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.p25', 'P25'), 'p25', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.median', 'Median'), 'median', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.p75', 'P75'), 'p75', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.p90', 'P90'), 'p90', strataId, cohortId),
+ this.getDecimal2Column(ko.i18n('columns.max', 'Max'), 'max', strataId, cohortId),
+ ];
+ }
+
+ getDefaultSort(columnsCount, cohortsCount) {
+ return [[ this.getDefaultColumns().length + 6, "desc" ]];
+ }
+ }
+
+ return DistributionStatConverter;
+});
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/PrevalenceStat.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/PrevalenceStat.js
new file mode 100644
index 000000000..6a7f080e3
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/PrevalenceStat.js
@@ -0,0 +1,22 @@
+define([
+], function () {
+
+ class PrevalenceStat {
+
+ constructor(stat) {
+ this.analysisId = stat.analysisId;
+ this.analysisName = stat.analysisName;
+ this.covariateId = stat.covariateId;
+ this.covariateName = stat.covariateName;
+ this.conceptId = stat.conceptId;
+ this.conceptName = stat.conceptName;
+ this.domainId = stat.domainId;
+ this.faType = stat.faType;
+ this.cohorts = stat.cohorts || [];
+ this.count = {};
+ this.pct = {};
+ }
+ }
+
+ return PrevalenceStat;
+});
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/PrevalenceStatConverter.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/PrevalenceStatConverter.js
new file mode 100644
index 000000000..01af14f80
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/conversion/PrevalenceStatConverter.js
@@ -0,0 +1,114 @@
+define([
+ 'knockout',
+ './BaseStatConverter',
+ './PrevalenceStat',
+ '../utils',
+ 'utils/CommonUtils'
+ ], function (
+ ko,
+ BaseStatConverter,
+ PrevalenceStat,
+ utils,
+ commonUtils
+ ) {
+
+ class PrevalenceStatConverter extends BaseStatConverter {
+
+ getResultObject(stat) {
+ return new PrevalenceStat(stat);
+ }
+
+ getRowId(stat) {
+ return stat.covariateId;
+ }
+
+ extractStrata(stat) {
+ return { strataId: stat.strataId, strataName: stat.strataName};
+ }
+
+ convertFields(result, strataId, cohortId, stat, prefix) {
+ ['count', 'pct'].forEach(field => {
+ const statName = prefix ? prefix + field.charAt(0).toUpperCase() + field.slice(1) : field;
+ this.setNestedValue(result, field, strataId, cohortId, stat[statName]);
+ });
+ }
+
+ convertCompareFields(result, strataId, stat) {
+ this.convertFields(result, strataId, stat.targetCohortId, stat, "target");
+ this.convertFields(result, strataId, stat.comparatorCohortId, stat, "comparator");
+ }
+
+ getDefaultColumns(analysis) {
+ return [
+ this.getCovNameColumn(analysis),
+ this.getExploreColumn(),
+ {
+ title: ko.i18n('columns.conceptId', 'Concept ID'),
+ data: 'conceptId',
+ render: (d, t, r) => {
+ if (r.conceptId === null || r.conceptId === undefined) {
+ return 'N/A';
+ } else {
+ return `${r.conceptId}`
+ }
+ }
+ }
+ ];
+ }
+
+ getReportColumns(strataId, cohortId) {
+ return [
+ this.getCountColumn(ko.i18n('columns.count', 'Count'), 'count', strataId, cohortId),
+ this.getPctColumn(ko.i18n('columns.pct', 'Pct'), 'pct', strataId, cohortId)
+ ];
+ }
+
+ getCovNameColumn(analysis) {
+ let covNameColumn = {
+ title: ko.i18n('columns.covariate', 'Covariate'),
+ data: 'covariateName',
+ className: this.classes('col-prev-title'),
+ render: (d, t, { covariateName, faType }) => utils.extractMeaningfulCovName(covariateName, faType),
+ xssSafe:false,
+ };
+ if (analysis && analysis.rawAnalysisName === 'DemographicsAgeGroup') {
+ covNameColumn.type = 'range';
+ }
+ return covNameColumn;
+ }
+
+ getExploreColumn() {
+ return {
+ title: ko.i18n('columns.explore', 'Explore'),
+ data: 'explore',
+ className: this.classes('col-explore'),
+ render: (d, t, r) => {
+ const stat = r;
+ let html;
+ if (stat && stat.analysisId && (stat.domainId !== undefined && stat.domainId !== 'DEMOGRAPHICS')) {
+ if (stat.cohorts.length > 1) {
+ html = ``;
+ html += `
`;
+ html += "
";
+ } else {
+ html = name + ``;
+ }
+ } else {
+ html = "N/A";
+ }
+ return html;
+ },
+ xssSafe:false,
+ };
+ }
+ }
+
+ return PrevalenceStatConverter;
+ });
+
\ No newline at end of file
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/demographic-report.html b/js/pages/cohort-definitions/components/reporting/cohort-reports/demographic-report.html
new file mode 100644
index 000000000..f17effbab
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/demographic-report.html
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ Std Diff |
+
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ loading
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/demographic-report.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/demographic-report.js
new file mode 100644
index 000000000..3c9e0332f
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/demographic-report.js
@@ -0,0 +1,400 @@
+define([
+ 'knockout',
+ 'services/CohortDefinition',
+ 'pages/cohort-definitions/components/reporting/cohort-reports/conversion/PrevalenceStatConverter',
+ 'pages/cohort-definitions/components/reporting/cohort-reports/conversion/DistributionStatConverter',
+ 'pages/cohort-definitions/components/reporting/cohort-reports/conversion/ComparativeDistributionStatConverter',
+ 'pages/characterizations/utils',
+ 'text!./demographic-report.html',
+ 'appConfig',
+ 'services/AuthAPI',
+ 'components/Component',
+ 'utils/AutoBind',
+ 'utils/CommonUtils',
+ './utils',
+ 'numeral',
+ 'lodash',
+ 'd3',
+ 'components/visualizations/filter-panel/utils',
+ 'components/conceptset/ConceptSetStore',
+ 'services/MomentAPI',
+ 'services/Source',
+ 'utils/CsvUtils',
+ 'services/Vocabulary',
+ 'atlas-state',
+ 'utils/ExceptionUtils',
+ 'services/file',
+ './explore-prevalence',
+ 'less!./demographic-report.less',
+ 'components/visualizations/filter-panel/filter-panel',
+ 'components/visualizations/line-chart',
+ 'components/charts/scatterplot',
+ 'components/charts/splitBoxplot',
+ 'components/charts/horizontalBoxplot',
+ 'd3-scale-chromatic',
+], function (
+ ko,
+ CohortDefinitionService,
+ PrevalenceStatConverter,
+ DistributionStatConverter,
+ ComparativeDistributionStatConverter,
+ pageUtils,
+ view,
+ config,
+ authApi,
+ Component,
+ AutoBind,
+ commonUtils,
+ utils,
+ numeral,
+ lodash,
+ d3,
+ filterUtils,
+ ConceptSetStore,
+ momentAPI,
+ SourceService,
+ CsvUtils,
+ vocabularyProvider,
+ sharedState,
+ exceptionUtils,
+ FileService
+) {
+
+ const TYPE_PREVALENCE = 'prevalence';
+
+ class DemographicReportView extends AutoBind(Component) {
+
+ constructor(params) {
+ super();
+ this.reportType = params.reportType;
+ this.cohortId = params.cohortId;
+ this.ccGenerateId = params.ccGenerateId;
+ this.prevalenceStatConverter = new PrevalenceStatConverter(this.classes);
+ this.distributionStatConverter = new DistributionStatConverter(this.classes);
+ this.comparativeDistributionStatConverter = new ComparativeDistributionStatConverter(this.classes);
+ this.conceptSetStore = ConceptSetStore.characterization();
+ this.currentConceptSet = ko.pureComputed(() => this.conceptSetStore.current());
+ this.loading = ko.observable(false);
+
+ this.design = ko.observable({});
+ this.executionId = ko.observable();
+ this.loadedExecutionId = null;
+ this.data = ko.observable([]);
+ this.domains = ko.observableArray();
+ this.filterList = ko.observableArray([]);
+ this.selectedItems = ko.pureComputed(() => filterUtils.getSelectedFilterValues(this.filterList()));
+ this.selectedItems.subscribe(() =>this.updateData);
+ this.analysisList = ko.observableArray([]);
+ this.canExportAll = ko.pureComputed(() => this.data().analyses && this.data().analyses.length > 0);
+ this.source = ko.pureComputed(() => {
+ return sharedState.sources().find(s => s.sourceKey === params.sourceKey());
+ });
+ this.stratifiedByTitle = ko.pureComputed(() => this.design().stratifiedBy || '');
+
+ this.groupedScatterColorScheme = d3.schemeCategory10;
+ this.scatterXScale = d3.scaleLinear().domain([0, 100]);
+ this.scatterYScale = d3.scaleLinear().domain([0, 100]);
+
+ this.executionDesign = ko.observable();
+ this.isExecutionDesignShown = ko.observable();
+ this.isExplorePrevalenceShown = ko.observable();
+ this.explorePrevalence = ko.observable();
+ this.explorePrevalenceTitle = ko.observable();
+ this.prevalenceStatData = ko.observableArray();
+ this.thresholdValuePct = ko.observable();
+ this.newThresholdValuePct = ko.observable().extend({ regexp: { pattern: '^(0*100{1,1}\\.?((?<=\\.)0*)?%?$)|(^0*\\d{0,2}\\.?((?<=\\.)\\d*)?%?)$', allowEmpty: false } });
+ this.showEmptyResults = ko.observable();
+ this.totalResultsCount = ko.observable();
+ this.resultsCountFiltered = ko.observable();
+ this.downloading = ko.observableArray();
+ this.tableOptions = commonUtils.getTableOptions('M');
+ this.datatableLanguage = ko.i18n('datatable.language');
+
+ this.loadData();
+ }
+
+ isResultDownloading(analysisName) {
+ return ko.computed(() => this.downloading().indexOf(analysisName) >= 0);
+ }
+
+ isRowGreyed(element, stat) {
+ if (stat.stdDiff && Math.abs(stat.stdDiff) < 0.1) {
+ element.classList.add(this.classes('greyed-row').trim());
+ }
+ }
+
+ getButtonsConfig(type, analysis) {
+ const buttons = [];
+
+ // buttons.push({
+ // text: ko.i18n('common.export', 'Export')(),
+ // action: () => this.exportCSV(analysis, false)
+ // });
+
+ // if (analysis.cohorts.length === 2) {
+ // buttons.push({
+ // text: ko.i18n('cc.viewEdit.results.table.buttons.exportComparison', 'Export comparison')(),
+ // action: () => this.exportCSV(analysis, true),
+ // });
+ // }
+
+ return buttons;
+ }
+
+ formatDate(date) {
+ return momentAPI.formatDateTimeUTC(date);
+ }
+
+ updateThreshold() {
+ this.loadData();
+ }
+
+ resultCountText() {
+ const values = { resultsCountFiltered: this.resultsCountFiltered(), totalResultsCount: this.resultsCountFiltered() }; // this.totalResultsCount()
+ return ko.i18nformat('cc.viewEdit.results.threshold.text', 'Viewing most prevalent <%=resultsCountFiltered%> of total <%=totalResultsCount%> records', values);
+ }
+
+ showExecutionDesign() {
+ this.executionDesign(null);
+ this.isExecutionDesignShown(true);
+ CohortDefinitionService
+ .loadExportDesignByGeneration(this.executionId())
+ .then(res => {
+ this.executionDesign(res);
+ this.loading(false);
+ });
+ }
+
+ exploreByFeature({covariateName, analysisId, covariateId, cohorts, ...o}, index) {
+ const {cohortId, cohortName} = cohorts[index];
+ this.explorePrevalence({executionId: this.executionId(), analysisId, cohortId, covariateId, cohortName});
+ this.explorePrevalenceTitle(ko.i18n('cc.viewEdit.results.exploring', 'Exploring')() + ' ' + covariateName);
+ this.isExplorePrevalenceShown(true);
+ }
+
+ async createNewSet(analysis) {
+ this.loading(true);
+ const conceptIds = this.extractConceptIds(analysis);
+ const items = await vocabularyProvider.getConceptsById(conceptIds);
+ await this.initConceptSet(items.data);
+ this.showConceptSet();
+ this.loading(false);
+ }
+
+ extractConceptIds(analysis) {
+ const conceptIds = [];
+ analysis.data.forEach(r => {
+ if (r.conceptId > 0) {
+ conceptIds.push(r.conceptId);
+ }
+ });
+ return conceptIds;
+ }
+
+ showConceptSet() {
+ commonUtils.routeTo('/conceptset/0/details');
+ }
+
+ async initConceptSet(conceptSetItems) {
+ this.currentConceptSet({
+ name: ko.observable('New Concept Set'),
+ id: 0,
+ });
+ this.currentConceptSetSource('repository');
+ for (let i = 0; i < conceptSetItems.length; i++) {
+ if (!sharedState.selectedConceptsIndex[conceptSetItems[i].CONCEPT_ID]) {
+ let conceptSetItem = commonUtils.createConceptSetItem(conceptSetItems[i]);
+ sharedState.selectedConceptsIndex[conceptSetItems[i].CONCEPT_ID] = {
+ isExcluded: conceptSetItem.isExcluded,
+ includeDescendants: conceptSetItem.includeDescendants,
+ includeMapped: conceptSetItem.includeMapped,
+ };
+ sharedState.selectedConcepts.push(conceptSetItem);
+ }
+ }
+ }
+
+ toggleEmptyResults() {
+ this.showEmptyResults(!this.showEmptyResults());
+ this.updateData();
+ }
+
+ async loadData() {
+ this.loading(true);
+
+ Promise.all([
+ CohortDefinitionService.getReport(this.cohortId(), this.source().sourceKey, this.reportType, this.ccGenerateId())
+ ]).then(([
+ generationResults
+ ]) => {
+ const count = generationResults?.demographicsStats?.length ? (generationResults.demographicsStats.reduce((prev, curr) => [...prev, ...curr.items],[]) || []).length : 0;
+ this.thresholdValuePct((generationResults.prevalenceThreshold || 0.01) * 100);
+ this.newThresholdValuePct(this.thresholdValuePct());
+ this.showEmptyResults(generationResults.showEmptyResults || null);
+ this.resultsCountFiltered(generationResults.count || count);
+ this.getData(generationResults?.demographicsStats);
+ this.loading(false);
+ });
+ }
+
+ getData(resultsList) {
+ const result = {
+ ...this.data(),
+ sourceId: this.source().sourceId,
+ sourceKey: this.source().sourceKey,
+ sourceName: this.source().sourceName,
+ analyses: lodash.sortBy(
+ lodash.uniqBy(
+ resultsList?.map(r => ({
+ analysisId: r.analysisId,
+ domainId: this.design() && this.design().featureAnalyses && !r.isSummary ?
+ (this.design().featureAnalyses.find(fa => fa.id === r.id) || { })[ 'domain' ] : null,
+ rawAnalysisName: r.analysisName,
+ analysisName: this.getAnalysisName(r.analysisName, { faType: r.faType, statType: r.resultType }),
+ cohorts: r.cohorts,
+ domainIds: r.domainIds,
+ type: r.resultType.toLowerCase(),
+ isSummary: r.isSummary,
+ isComparative: r.isComparative,
+ items: r.items,
+ })),
+ 'analysisId'
+ ),
+ [( a ) => { return a.analysisId || ''}], ['desc']
+ ),
+ }
+ this.data(result);
+ this.prepareTabularData();
+ }
+
+ getAnalysisName(rawName, { faType, statType }) {
+ return rawName + ((faType === 'PRESET' && statType.toLowerCase() === TYPE_PREVALENCE) ? ` (prevalence > ${this.newThresholdValuePct()}%)` : '');
+ }
+
+ // async exportCSV(analysis, isComparative) {
+ // try {
+ // this.downloading.push(analysis.analysisName);
+ // let filterParams = filterUtils.getSelectedFilterValues(this.filterList());
+ // let params = {
+ // cohortIds: analysis.cohorts.map(c => c.cohortId),
+ // analysisIds: analysis.analysisId ? [analysis.analysisId] : filterParams.analyses,
+ // domainIds: analysis.domainIds,
+ // isSummary: analysis.isSummary,
+ // isComparative: isComparative,
+ // thresholdValuePct: this.thresholdValuePct() / 100,
+ // showEmptyResults: !!this.showEmptyResults(),
+ // };
+ // await FileService.loadZip(`${config.api.url}cohort-characterization/generation/${this.executionId()}/result/export`,
+ // `characterization_${this.characterizationId()}_execution_${this.executionId()}_report.zip`, 'POST', params);
+
+ // }catch (e) {
+ // alert(exceptionUtils.translateException(e));
+ // } finally {
+ // this.downloading.remove(analysis.analysisName);
+ // }
+ // }
+
+ findDomainById(domainId) {
+ const domain = this.domains().find(d => d.id === domainId);
+ return domain || {name: 'Unknown'};
+ }
+
+ sortedStrataNames(strataNames) {
+ return utils.sortedStrataNames(strataNames, true);
+ }
+
+ prepareTabularData() {
+ if (!this.data().analyses || this.data().analyses.length === 0) {
+ this.analysisList([]);
+ return;
+ }
+
+ const designStratas = this.showEmptyResults() ? this.design().stratas.map(s => ({ strataId: s.id, strataName: s.name })) : null;
+
+ const convertedData = this.data().analyses.map(analysis => {
+ let converter;
+ if (analysis.type === TYPE_PREVALENCE) {
+ converter = this.prevalenceStatConverter;
+ } else {
+ if (analysis.isComparative) {
+ converter = this.comparativeDistributionStatConverter;
+ } else {
+ converter = this.distributionStatConverter;
+ }
+ }
+ return converter.convertAnalysisToTabularData(analysis, designStratas);
+ });
+ this.analysisList(convertedData);
+ }
+
+ tooltipBuilder(d) {
+ return `
+
${ko.i18n('cc.viewEdit.results.series', 'Series')()}: ${d.seriesName}
+ ${ko.i18n('cc.viewEdit.results.covariate', 'Covariate')()}: ${d.covariateName}
+ X: ${d3.format('.2f')(d.xValue)}%
+ Y: ${d3.format('.2f')(d.yValue)}%
+ `;
+ }
+
+ convertScatterplotData(analysis) {
+ const seriesData = lodash.groupBy(analysis.data, 'analysisName');
+ const firstCohortId = analysis.cohorts[0].cohortId;
+ const secondCohortId = analysis.cohorts[1].cohortId;
+ return Object.keys(seriesData).map(key => ({
+ name: key,
+ values: seriesData[key].filter(rd => rd.pct[0][firstCohortId] && rd.pct[0][secondCohortId]).map(rd => ({
+ covariateName: rd.covariateName,
+ xValue: rd.pct[0][firstCohortId] || 0,
+ yValue: rd.pct[0][secondCohortId] || 0
+ })),
+ }));
+ }
+
+ getBoxplotStruct(cohort, stat) {
+ return {
+ Category: cohort.cohortName,
+ min: stat.min[0][cohort.cohortId],
+ max: stat.max[0][cohort.cohortId],
+ median: stat.median[0][cohort.cohortId],
+ LIF: stat.p10[0][cohort.cohortId],
+ q1: stat.p25[0][cohort.cohortId],
+ q3: stat.p75[0][cohort.cohortId],
+ UIF: stat.p90[0][cohort.cohortId]
+ };
+ }
+
+ convertBoxplotData(analysis) {
+ return [{
+ target: this.getBoxplotStruct(analysis.cohorts[0], analysis.data[0]),
+ compare: this.getBoxplotStruct(analysis.cohorts[1], analysis.data[0]),
+ }];
+ }
+
+ convertHorizontalBoxplotData(analysis) {
+ return analysis.cohorts.map(cohort => {
+ return this.getBoxplotStruct(cohort, analysis.data[0]);
+ });
+ }
+
+ prepareLegendBoxplotData (analysis) {
+ const cohortNames = analysis.cohorts.map(d => d.cohortName);
+ const legendColorsSchema = d3.scaleOrdinal().domain(cohortNames)
+ .range(utils.colorHorizontalBoxplot);
+
+ const legendColors = cohortNames.map(cohort => {
+ return {
+ cohortName: cohort,
+ cohortColor: legendColorsSchema(cohort)
+ };
+ });
+ return legendColors.reverse();
+ }
+
+ analysisTitle(data) {
+ const strata = data.stratified ? (' / stratified by ' + this.stratifiedByTitle()): '';
+ return (data.domainId ? (data.domainId + ' / ') : '') + data.analysisName + strata;
+ }
+ }
+
+ return commonUtils.build('demographic-report', DemographicReportView, view);
+});
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/demographic-report.less b/js/pages/cohort-definitions/components/reporting/cohort-reports/demographic-report.less
new file mode 100644
index 000000000..dfffff11a
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/demographic-report.less
@@ -0,0 +1,297 @@
+.demographic-report {
+
+ &__header {
+ align-items: center;
+ display: flex;
+ }
+
+ &__title {
+ align-items: center;
+ display: flex;
+ margin: 0;
+ margin-top: 1rem;
+ font-size: 1.8rem;
+ }
+
+ &__title-separator {
+ font-size: 1rem;
+ padding-left: 1rem;
+ padding-right: .75rem;
+ }
+
+ &__toolbar {
+ align-items: center;
+ display: flex;
+ margin-top: 2rem;
+ }
+
+ &__detail-list, &__display-options-list {
+ display: flex;
+ list-style: none;
+ margin-bottom: 0;
+ padding: 0;
+ }
+
+ &__detail, &__display-option {
+ &:not(:last-child) {
+ margin-right: 1.5rem;
+ }
+ }
+
+ &__detail {
+ &-label, &-value {
+ font-size: 1.3rem;
+ margin: 0;
+ }
+ &-label {
+ font-weight: 500;
+ }
+ a&-value {
+ cursor: pointer;
+ }
+ }
+
+ &__threshold {
+ margin-left: auto;
+ }
+
+ &__threshold-setter {
+ align-items: center;
+ display: flex;
+ }
+
+ &__threshold-label {
+ font-size: 1.1rem;
+ margin-bottom: 0;
+ margin-right: 0.75rem;
+ white-space: nowrap;
+ }
+
+ &__threshold-input {
+ font-size: 1.1rem;
+ margin-bottom: 0;
+ margin-right: 0.5rem;
+ width: 7rem;
+ }
+
+ &__threshold-submit {
+ margin-left: 0.75rem;
+ }
+
+ &__threshold-result-descr {
+ font-size: 1.1rem;
+ float: right;
+ margin-top: 0.75rem;
+ }
+
+ &__display-options-list {
+ margin-left: auto;
+ }
+
+ &__display-option {
+ font-size: 1.3rem;
+ }
+
+ &__display-option-label {
+ font-weight: 600;
+ }
+
+ &__display-option-link {
+ cursor: pointer;
+ }
+
+ &__filter {
+ margin-top: 2rem;
+ }
+
+ &__report-group {
+ margin-bottom: 3rem;
+ }
+
+ th {
+ vertical-align: middle !important;
+ }
+
+ &__report {
+ margin-top: 2rem;
+
+ &--hidden-cohort-name {
+ .characterization-view-edit-results__cohort-name {
+ display: none;
+ }
+ }
+ }
+
+ &__analysis-name {
+ font-size: 1.6rem;
+ font-weight: 600;
+ }
+
+ &__cohort-name {
+ font-size: 1.4rem;
+ }
+
+ &__th-cov-count, &__th-cov-pct, &__th-diff {
+ width: 7.5rem !important;
+ }
+
+ &__th-cohort-name {
+ text-align: center;
+ }
+
+ &__analysis-results {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+ }
+
+ &__table-wrapper, &__chart-wrapper, &__table-wrapper-nodata {
+ flex-shrink: 0;
+ flex-basis: 0;
+ }
+
+ &__table-wrapper-nodata {
+ flex: 2;
+ width:100%;
+ .dt-buttons {
+ padding-bottom: 0;
+ padding-right: 0;
+ }
+ }
+
+ &__table-wrapper {
+ flex: 2;
+ width:100%;
+ .dt-buttons {
+ padding-bottom: 1rem;
+ padding-right: 1rem;
+ }
+ }
+
+ &__chart-wrapper {
+ flex: 1;
+ width: 100%;
+ margin-left: 2rem;
+ border: 1px solid #ccc;
+ padding: 1.5rem 1.5rem 0.45rem 0;
+ }
+
+ &__scatterplot {
+ }
+
+ &__boxplot {
+ svg {
+ padding-left: 1rem;
+ }
+
+ .boxplot .compare {
+ .median, line, rect, circle, .whisker {
+ stroke: #ff9315;
+ }
+ line, rect, circle {
+ stroke-width: 1px;
+ }
+ rect, circle {
+ fill: rgba(255, 147, 21, 0.67);
+ }
+ .whisker {
+ stroke-dasharray: 3, 3;
+ }
+ }
+ }
+ &__execution-design {
+ width: 100%;
+ height: 600px;
+ }
+ &__explore {
+ margin-top: .5rem;
+ &-link {
+ cursor: pointer;
+ }
+ }
+
+ &__report-table {
+ width: 100% !important;
+ height: fit-content; // needed if pct-cell has more than one-line height (to correctly fill it vertically)
+
+ td.pct-cell {
+ padding: 0 !important;
+ height: 100%;
+ }
+ div.pct-fill {
+ background-color: #afd9ee;
+ height: 100%;
+
+ div {
+ vertical-align: middle;
+ padding: 8px 10px;
+ }
+ }
+ }
+
+ &__greyed-row {
+ color: grey;
+ }
+
+ &__col-explore {
+ width: 5rem !important;
+ }
+
+ &__explore-dropdown {
+ position: relative;
+ text-align: center;
+ }
+
+ &__explore-caret {
+ margin-left: .4rem;
+ }
+
+ &__explore-menu-item {
+ cursor: pointer;
+ }
+
+ &__explore-menu-item-link {
+ max-width: 50rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__action-ico {
+ margin-left: 0.5rem;
+ }
+}
+
+.characterization-results-boxplot-container {
+ display: flex;
+ width: 50%;
+}
+
+.characterization-results-legend-container {
+ min-width: 300px;
+ flex: 0.5;
+ margin-left: 16px;
+}
+
+.characterization-results-container {
+ display: flex;
+ width: 100%;
+}
+.legend-header {
+ margin-top: 0;
+}
+.swatch {
+ width: 16px;
+ height: 16px;
+ margin: 6px 0;
+}
+
+.color-cell {
+ width: 24px;
+ vertical-align: baseline;
+}
+
+.legend-cohort-name {
+ font-size: 1rem;
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/explore-prevalence.html b/js/pages/cohort-definitions/components/reporting/cohort-reports/explore-prevalence.html
new file mode 100644
index 000000000..503c3fc4f
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/explore-prevalence.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+ Relation |
+ Distance |
+ Concept name |
+
+ |
+
+
+
+
+ Count |
+ Pct |
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/explore-prevalence.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/explore-prevalence.js
new file mode 100644
index 000000000..27d7c8838
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/explore-prevalence.js
@@ -0,0 +1,184 @@
+define([
+ 'knockout',
+ 'text!./explore-prevalence.html',
+ 'components/Component',
+ 'utils/CommonUtils',
+ 'utils/AutoBind',
+ 'services/CohortDefinition',
+ './utils',
+ 'numeral',
+ 'utils/CsvUtils',
+ 'less!./explore-prevalence.less',
+], function(
+ ko,
+ view,
+ Component,
+ commonUtils,
+ AutoBind,
+ CohortDefinitionService,
+ utils,
+ numeral,
+ CsvUtils,
+){
+
+ class ExplorePrevalence extends AutoBind(Component) {
+
+ constructor(params) {
+ super(params);
+ this.tableColumns = [
+ {
+ title: ko.i18n('columns.relationshipType', 'Relationship type'),
+ render: this.renderRelationship,
+ class: this.classes('col-type'),
+ },
+ {
+ title: ko.i18n('columns.distance', 'Distance'),
+ data: 'distance',
+ class: this.classes('col-distance'),
+ },
+ {
+ title: ko.i18n('columns.conceptName', 'Concept name'),
+ data: 'covariateName',
+ class: this.classes('col-concept'),
+ render: (d, t, { covariateName, faType }) => pageUtils.extractMeaningfulCovName(covariateName, faType)
+ },
+ ];
+ this.data = ko.observableArray();
+ this.loading = ko.observable();
+ this.explore = params.explore;
+ this.cohortId = this.explore.cohortId;
+ this.cohortName = this.explore.cohortName;
+ this.exploring = ko.observable();
+ this.relations = ko.computed(() => this.prepareTabularData(this.data()));
+ this.tableOptions = commonUtils.getTableOptions('M');
+ this.exploringTitle = ko.pureComputed(() => this.exploring() ? ko.i18n('cc.viewEdit.executions.prevalenceStatConverter.exploringConceptHierarchyFor', 'Exploring concept hierarchy for: ')() + ' ' + this.exploring() : null );
+ this.loadData(this.explore);
+ }
+
+ loadData({executionId, analysisId, cohortId, covariateId}) {
+ this.loading(true);
+ return CohortDefinitionService.getPrevalenceStatsByGeneration(executionId, analysisId, cohortId, covariateId)
+ .then(res => this.data(res.map(v => ({...v, executionId}))))
+ .finally(() => this.loading(false));
+ }
+
+ getCountColumn(strata) {
+ return {
+ title: ko.i18n('columns.count', 'Count'),
+ class: this.classes('col-count'),
+ render: (s, p, d) => numeral(d.count[strata] || 0).format(),
+ };
+ }
+
+ getPctColumn(strata, idx) {
+ return {
+ title: ko.i18n('columns.pct', 'Pct'),
+ class: this.classes('col-pct'),
+ render: (s, p, d) => {
+ const pct = utils.formatPct(d.pct[strata] || 0);
+ return ``;
+ },
+ };
+ }
+
+ prepareTabularData(data) {
+ const columns = [
+ ...this.tableColumns,
+ ];
+ let stratas = {
+ };
+ let stats = {};
+ data.forEach(st => {
+ if (stats[st.covariateId] === undefined) {
+ stats[st.covariateId] = {
+ ...st,
+ count: {},
+ pct: {},
+ };
+ }
+ if (stratas[st.strataId] === undefined) {
+ stratas[st.strataId] = st.strataName || ko.i18n('cc.viewEdit.executions.prevalenceStatConverter.allStrata', 'All strata')();
+ }
+ const stat = stats[st.covariateId];
+ stat.count[st.strataId] = st.count;
+ stat.pct[st.strataId] = st.avg * 100;
+ });
+ Object.keys(stratas).forEach(strataId => {
+ columns.push(this.getCountColumn(strataId));
+ columns.push(this.getPctColumn(strataId));
+ });
+ stats = Object.values(stats);
+ stratas = Object.values(stratas);
+ return {
+ stats,
+ stratas,
+ columns,
+ };
+ }
+
+ exploreByFeature(data) {
+ this.loadData(data).then(() => {
+ if (data.covariateId !== this.explore.covariateId) {
+ this.exploring(data.covariateName);
+ } else {
+ this.exploring(null);
+ }
+ });
+ }
+
+ getButtonsConfig() {
+ const buttons = [];
+
+ buttons.push({
+ text: ko.i18n('common.export', 'Export')(),
+ action: () => this.exportTable()
+ });
+
+ return buttons;
+ }
+
+ exportTable() {
+ const exprt = this.relations().stats.map(stat => {
+ return ({
+ 'Relationship Type': this.getRelationshipTypeFromDistance(stat.distance),
+ 'Distance' : stat.distance,
+ 'Covariate short name': stat.conceptName,
+ 'Count': stat.count[stat.strataId],
+ 'Percent': stat.pct[stat.strataId],
+ 'Strata ID': stat.strataId,
+ 'Strata name': stat.strataName,
+ 'Analysis ID': stat.analysisId,
+ 'Analysis name': stat.analysisName,
+ 'Covariate ID': stat.covariateId,
+ 'Covariate name': stat.covariateName
+ });
+ });
+ exprt.sort((a,b) => b["Distance"] - a["Distance"]);
+ CsvUtils.saveAsCsv(exprt);
+ }
+
+ resetExploring() {
+ this.loadData(this.explore).then(() => this.exploring(null));
+ }
+
+ renderRelationship(data, type, row) {
+ const distance = row.distance;
+ const rel = this.getRelationshipTypeFromDistance(distance);
+ const cls = this.classes({element: 'explore', modifiers: distance === 0 ? 'disabled' : '' });
+ const binding = distance !== 0 ? 'click: () => $component.exploreByFeature({...$data, cohortId: $component.cohortId})' : '';
+ return " " + rel;
+ }
+
+ getRelationshipTypeFromDistance(distance) {
+ return distance > 0 ?
+ ko.i18n('cc.viewEdit.executions.prevalenceStatConverter.ancestor', 'Ancestor')() :
+ distance < 0 ?
+ ko.i18n('cc.viewEdit.executions.prevalenceStatConverter.descendant', 'Descendant')() :
+ ko.i18n('cc.viewEdit.executions.prevalenceStatConverter.selected', 'Selected')();
+ }
+
+ }
+
+ commonUtils.build('explore-prevalence-demographic', ExplorePrevalence, view);
+
+});
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/explore-prevalence.less b/js/pages/cohort-definitions/components/reporting/cohort-reports/explore-prevalence.less
new file mode 100644
index 000000000..9853d2f11
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/explore-prevalence.less
@@ -0,0 +1,57 @@
+.explore-prevalence {
+ &__explore {
+ margin-right: 0.5rem;
+
+ &--disabled {
+ opacity: 0.5;
+ cursor: auto;
+ }
+ margin-top: .5rem;
+ cursor: pointer;
+ }
+ &__reset {
+ margin-right: 0.3rem;
+ color: #ee0000;
+ cursor: pointer;
+ }
+ &__exploring-pane {
+ margin: 0.5rem 0 1.5rem;
+ }
+ &__exploring-title {
+ font-weight: bold;
+ }
+ &__col-type {
+ width: 12rem;
+ }
+ &__col-distance {
+ width: 4rem;
+ }
+ &__col-concept {
+ width: 22rem;
+ }
+ &__col-count {
+ width: 4rem;
+ }
+ &__col-pct {
+ width: 4rem;
+ }
+ td&__col-pct {
+ padding: 0 !important;
+ height: 100%;
+ }
+
+ .dt-buttons {
+ padding-bottom: 1rem;
+ padding-right: 1rem;
+ }
+
+ div.pct-fill {
+ background-color: #afd9ee;
+ height: 100%;
+
+ div {
+ vertical-align: middle;
+ padding: 8px 10px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/inclusion-report.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/inclusion-report.js
index b2273fc0c..27326bd2c 100644
--- a/js/pages/cohort-definitions/components/reporting/cohort-reports/inclusion-report.js
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/inclusion-report.js
@@ -4,7 +4,8 @@ define([
'./const',
'utils/CommonUtils',
'text!./inclusion-report.html',
- './feasibility-report-viewer-with-header'
+ './feasibility-report-viewer-with-header',
+ './demographic-report'
], function (
ko,
Component,
@@ -17,8 +18,8 @@ define([
constructor(params) {
super();
- this.tabs = [
- {
+ this.tabs = ko.computed(() => {
+ return [{
title: ko.i18n('cohortDefinitions.cohortreports.tabs.byPerson', 'By Person'),
componentName: 'feasibility-report-viewer-with-header',
componentParams: { ...params, reportType: constants.INCLUSION_REPORT.BY_PERSON },
@@ -27,8 +28,8 @@ define([
title: ko.i18n('cohortDefinitions.cohortreports.tabs.byEvents', 'By All Events'),
componentName: 'feasibility-report-viewer-with-header',
componentParams: { ...params, reportType: constants.INCLUSION_REPORT.BY_EVENT },
- }
- ];
+ }]
+ });
}
}
diff --git a/js/pages/cohort-definitions/components/reporting/cohort-reports/utils.js b/js/pages/cohort-definitions/components/reporting/cohort-reports/utils.js
new file mode 100644
index 000000000..4ea5de500
--- /dev/null
+++ b/js/pages/cohort-definitions/components/reporting/cohort-reports/utils.js
@@ -0,0 +1,52 @@
+define([ 'numeral' , './const',], function(
+ numeral,
+ constants
+){
+
+ const formatStdDiff = (val) => numeral(val).format('0,0.0000');
+
+ const formatPct = (val) => numeral(val).format('0.00') + '%';
+
+ const colorHorizontalBoxplot = [
+ "#ff9315",
+ "#0d61ff",
+ "gold",
+ "blue",
+ "green",
+ "red",
+ "black",
+ "orange",
+ "brown",
+ "grey",
+ "slateblue",
+ "grey1",
+ "darkgreen"
+ ];
+
+ function extractMeaningfulCovName(fullName, faType = constants.feAnalysisTypes.CRITERIA) {
+ if ([constants.feAnalysisTypes.CRITERIA_SET, constants.feAnalysisTypes.CUSTOM_FE].includes(faType)) {
+ return fullName;
+ }
+ let nameParts = fullName.split(":");
+ if (nameParts.length < 2) {
+ nameParts = fullName.split("=");
+ }
+ if (nameParts.length !== 2) {
+ return fullName;
+ } else {
+ return nameParts[1];
+ }
+ }
+
+ function sortedStrataNames(strataNames, filter = null) {
+ return Array.from(strataNames).map(s => ({id: s[0], name: s[1]})).filter(s => !filter || s.id !== 0).sort((a,b) => a.id - b.id);
+ }
+
+ return {
+ formatPct,
+ formatStdDiff,
+ colorHorizontalBoxplot,
+ sortedStrataNames,
+ extractMeaningfulCovName
+ };
+});
\ No newline at end of file
diff --git a/js/services/CohortDefinition.js b/js/services/CohortDefinition.js
index 13ec4a567..68c27f879 100644
--- a/js/services/CohortDefinition.js
+++ b/js/services/CohortDefinition.js
@@ -100,9 +100,8 @@ define(function (require, exports) {
}
- function generate(cohortDefinitionId, sourceKey) {
- return httpService.doGet(`${config.webAPIRoot}cohortdefinition/${cohortDefinitionId}/generate/${sourceKey}`);
- }
+ function generate(cohortDefinitionId, sourceKey, withDemographic) {
+ return httpService.doGet(`${config.webAPIRoot}cohortdefinition/${cohortDefinitionId}/generate/${sourceKey}?demographic=${withDemographic}`); }
function cancelGenerate(cohortDefinitionId, sourceKey) {
@@ -123,9 +122,10 @@ define(function (require, exports) {
return infoPromise;
}
- function getReport(cohortDefinitionId, sourceKey, modeId) {
+ function getReport(cohortDefinitionId, sourceKey, modeId, ccGenerateId) {
+ const urlGetReportDemographic = `${config.webAPIRoot}cohortdefinition/${(cohortDefinitionId || '-1')}/report/${sourceKey}?mode=${modeId || 0}&ccGenerateId=${ccGenerateId}`
var reportPromise = $.ajax({
- url: `${config.webAPIRoot}cohortdefinition/${(cohortDefinitionId || '-1')}/report/${sourceKey}?mode=${modeId || 0}`,
+ url: modeId !== 2 ? `${config.webAPIRoot}cohortdefinition/${(cohortDefinitionId || '-1')}/report/${sourceKey}?mode=${modeId || 0}` : urlGetReportDemographic,
error: function (error) {
console.log("Error: " + error);
authApi.handleAccessDenied(error);
@@ -183,6 +183,12 @@ define(function (require, exports) {
}).then(({ data }) => data);
}
+ function getPrevalenceStatsByGeneration(generationId, analysisId, cohortId, covariateId) {
+ return httpService
+ .doGet(config.webAPIRoot + `cohort-characterization/generation/${generationId}/explore/prevalence/${analysisId}/${cohortId}/${covariateId}`)
+ .then(res => res.data);
+ }
+
var api = {
getCohortDefinitionList,
saveCohortDefinition,
@@ -203,7 +209,8 @@ define(function (require, exports) {
getVersions,
getVersion,
updateVersion,
- copyVersion
+ copyVersion,
+ getPrevalenceStatsByGeneration
};
return api;