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 `
${res}
`; + } + 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
+
+ +
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+

Cohort Legend

+
+ + + + + +
+
+
+
+ +
+
+ +
+ +
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 @@ +
+ +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
RelationDistanceConcept name
CountPct
+ +
\ 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 `
${pct}
`; + }, + }; + } + + 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;