diff --git a/package.json b/package.json index cdb232aa253..a6096840b3b 100644 --- a/package.json +++ b/package.json @@ -234,6 +234,7 @@ "pushstate-server": "3.0.1", "query-string": "^6.13.6", "raw-loader": "^0.5.1", + "range-ts": "^0.1.5", "rc-animate": "^3.1.1", "rc-tooltip": "^5.0.2", "rc-trigger": "^5.2.1", diff --git a/src/pages/studyView/StudyViewUtils.spec.tsx b/src/pages/studyView/StudyViewUtils.spec.tsx index eade96667c4..c76e2096b9d 100644 --- a/src/pages/studyView/StudyViewUtils.spec.tsx +++ b/src/pages/studyView/StudyViewUtils.spec.tsx @@ -1,39 +1,53 @@ import { assert } from 'chai'; import * as React from 'react'; import { + annotationFilterActive, calcIntervalBinValues, calculateLayout, calculateNewLayoutForFocusedChart, + ChartMeta, chartMetaComparator, + ChartMetaDataTypeEnum, clinicalDataCountComparator, customBinsAreValid, + DataBin, + driverTierFilterActive, filterCategoryBins, filterIntervalBins, filterNumericalBins, findSpot, formatFrequency, formatNumericalTickValues, + formatRange, + geneFilterQueryFromOql, + geneFilterQueryToOql, generateCategoricalData, generateMatrixByLayout, generateNumericalData, + getBinName, getClinicalDataCountWithColorByCategoryCounts, getClinicalDataCountWithColorByClinicalDataCount, - getDataIntervalFilterValues, getClinicalEqualityFilterValuesByString, getCNAByAlteration, + getDataIntervalFilterValues, getDefaultChartTypeByClinicalAttribute, getExponent, getFilteredSampleIdentifiers, getFilteredStudiesWithSamples, getFrequencyStr, + getGroupedClinicalDataByBins, + getNonZeroUniqueBins, + getPatientIdentifiers, getPositionXByUniqueKey, getPositionYByUniqueKey, getPriorityByClinicalAttribute, getQValue, getRequestedAwaitPromisesForClinicalData, getSamplesByExcludingFiltersOnChart, + getStudyViewTabId, getVirtualStudyDescription, intervalFiltersDisplayValue, + isDataBinSelected, isEveryBinDistinct, isFocusedChartShrunk, isLogScaleByDataBins, @@ -42,39 +56,26 @@ import { makePatientToClinicalAnalysisGroup, needAdditionShiftForLogScaleBarChart, pickClinicalDataColors, - showOriginStudiesInSummaryDescription, shouldShowChart, + showOriginStudiesInSummaryDescription, + statusFilterActive, + StudyViewFilterWithSampleIdentifierFilters, toFixedDigit, + updateCustomIntervalFilter, updateGeneQuery, - StudyViewFilterWithSampleIdentifierFilters, - ChartMeta, - ChartMetaDataTypeEnum, - getStudyViewTabId, - formatRange, - getBinName, - getGroupedClinicalDataByBins, updateSavedUserPreferenceChartIds, - getNonZeroUniqueBins, - DataBin, - getPatientIdentifiers, - geneFilterQueryFromOql, - geneFilterQueryToOql, - annotationFilterActive, - driverTierFilterActive, - statusFilterActive, - updateCustomIntervalFilter, } from 'pages/studyView/StudyViewUtils'; import { - Sample, - StudyViewFilter, - DataFilterValue, CancerStudy, ClinicalAttribute, + DataFilterValue, + Sample, + StudyViewFilter, } from 'cbioportal-ts-api-client'; import { StudyViewPageTabKeyEnum } from 'pages/studyView/StudyViewPageTabs'; import { SpecialChartsUniqueKeyEnum } from './StudyViewUtils'; import { Layout } from 'react-grid-layout'; -import sinon from 'sinon'; +import sinon, { spy } from 'sinon'; import internalClient from 'shared/api/cbioportalInternalClientInstance'; import { ChartDimension, ChartTypeEnum } from './StudyViewConfig'; import { MobxPromise } from 'mobxpromise'; @@ -88,13 +89,7 @@ import { ChartUserSetting, VirtualStudy, } from 'shared/api/session-service/sessionServiceModels'; -import { spy } from 'sinon'; -import { shallow, render, mount } from 'enzyme'; -import { - EditableSpan, - remoteData, - toPromise, -} from 'cbioportal-frontend-commons'; +import { remoteData, toPromise } from 'cbioportal-frontend-commons'; import { autorun, observable, runInAction } from 'mobx'; describe('StudyViewUtils', () => { @@ -1537,6 +1532,894 @@ describe('StudyViewUtils', () => { }); }); + describe('isDataBinSelected', () => { + const categoryFilter = { + value: 'Unknown', + } as DataFilterValue; + + const singlePointFilter = { + start: 14, + end: 14, + } as DataFilterValue; + + const startExclusiveEndInclusiveFilter = { + start: 14, + end: 15, + } as DataFilterValue; + + const startExclusiveOpenEndedFilter: DataFilterValue = { + start: 14, + value: '>', + } as DataFilterValue; + + const startInclusiveOpenEndedFilter: DataFilterValue = { + start: 14, + value: '>=', + } as DataFilterValue; + + const endExclusiveOpenStartFilter: DataFilterValue = { + end: 14, + value: '<', + } as DataFilterValue; + + const endInclusiveOpenStartFilter: DataFilterValue = { + end: 14, + value: '<=', + } as DataFilterValue; + + describe('test isDataBinSelected with a single point data bin for all filters', () => { + it('rejects a single point data bin for any categorical filter', () => { + const dataBin = { + start: 14, + end: 14, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [categoryFilter]), + '14 should be rejected by Unknown' + ); + }); + + it('accepts a single point data bin that falls into a single point filter', () => { + const dataBin = { + start: 14, + end: 14, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [singlePointFilter]), + '14 should be accepted by 14' + ); + }); + + it('rejects a single point data bin that does not fall into a start inclusive open ended filter', () => { + const dataBin = { + start: 13, + end: 13, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [singlePointFilter]), + '13 should be rejected by 14' + ); + }); + + it('accepts a single point data bin that falls into a start exclusive end inclusive filter', () => { + const dataBin = { + start: 15, + end: 15, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [ + startExclusiveEndInclusiveFilter, + ]), + '15 should be accepted by (14, 15]' + ); + }); + + it('rejects a single point data bin that does not fall into a start exclusive end inclusive filter', () => { + const dataBin = { + start: 14, + end: 14, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [ + startExclusiveEndInclusiveFilter, + ]), + '14 should be rejected by (14, 15]' + ); + }); + + it('accepts a single point data bin that falls into a start exclusive open ended filter', () => { + const dataBin = { + start: 15, + end: 15, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + '15 should be accepted by (14, Infinity)' + ); + }); + + it('rejects a single point data bin that does not fall into a start exclusive open ended filter', () => { + const dataBin = { + start: 14, + end: 14, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + '14 should be rejected by (14, Infinity)' + ); + }); + + it('accepts a single point data bin that falls into a start inclusive open ended filter', () => { + const dataBin = { + start: 14, + end: 14, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + '14 should be accepted by [14, Infinity)' + ); + }); + + it('rejects a single point data bin that does not fall into a start inclusive open ended filter', () => { + const dataBin = { + start: 13, + end: 13, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + '13 should be rejected by [14, Infinity)' + ); + }); + + it('accepts a single point data bin that falls into a end exclusive open start filter', () => { + const dataBin = { + start: 13, + end: 13, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + '13 should be accepted by (-Infinity, 14)' + ); + }); + + it('rejects a single point data bin that does not fall into an end exclusive open start filter', () => { + const dataBin = { + start: 14, + end: 14, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + '14 should be rejected by (-Infinity, 14)' + ); + }); + + it('accepts a single point data bin that falls into an end inclusive open start filter', () => { + const dataBin = { + start: 14, + end: 14, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + '14 should be accepted by (-Infinity, 14]' + ); + }); + + it('rejects a single point data bin that does not fall into an end inclusive open start filter', () => { + const dataBin = { + start: 15, + end: 15, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + '15 should be rejected by (-Infinity, 14]' + ); + }); + }); + + describe('test isDataBinSelected with a start exclusive end inclusive data bin for all filters', () => { + it('rejects a start exclusive end inclusive data bin for any categorical filter', () => { + const dataBin = { + start: 13, + end: 14, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [categoryFilter]), + '(13, 14] should be rejected by Unknown' + ); + }); + + it('rejects a start exclusive end inclusive data bin for any single point filter', () => { + const dataBin = { + start: 13, + end: 14, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [singlePointFilter]), + '(13, 14] should be accepted by 14' + ); + }); + + it('accepts a start exclusive end inclusive data bin that falls into a start exclusive end inclusive filter', () => { + const dataBin = { + start: 14, + end: 15, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [ + startExclusiveEndInclusiveFilter, + ]), + '(14, 15] should be accepted by (14, 15]' + ); + }); + + it('rejects a start exclusive end inclusive data bin that does not fall into a start exclusive end inclusive filter', () => { + const dataBin = { + start: 13, + end: 15, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [ + startExclusiveEndInclusiveFilter, + ]), + '(13, 15] should be rejected by (14, 15]' + ); + }); + + it('accepts a start exclusive end inclusive data bin that falls into a start exclusive open ended filter', () => { + const dataBin = { + start: 14, + end: 15, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + '(14, 15] should be accepted by (14, Infinity)' + ); + }); + + it('rejects a start exclusive end inclusive data bin that does not fall into a start exclusive open ended filter', () => { + const dataBin = { + start: 13, + end: 15, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + '(13, 15] should be rejected by (14, Infinity)' + ); + }); + + it('accepts a start exclusive end inclusive data bin that falls into a start inclusive open ended filter', () => { + const dataBin = { + start: 14, + end: 15, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + '(14, 15] should be accepted by [14, Infinity)' + ); + }); + + it('rejects a start exclusive end inclusive data bin that does not fall into a start inclusive open ended filter', () => { + const dataBin = { + start: 13, + end: 15, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + '(13, 15] should be rejected by [14, Infinity)' + ); + }); + + it('accepts a start exclusive end inclusive data bin that falls into a end exclusive open start filter', () => { + const dataBin = { + start: 12, + end: 13, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + '(12, 13] should be accepted by (-Infinity, 14)' + ); + }); + + it('rejects a start exclusive end inclusive data bin that does not fall into an end exclusive open start filter', () => { + const dataBin = { + start: 13, + end: 14, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + '(13, 14] should be rejected by (-Infinity, 14)' + ); + }); + + it('accepts a start exclusive end inclusive data bin that falls into an end inclusive open start filter', () => { + const dataBin = { + start: 13, + end: 14, + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + '(13, 14] should be accepted by (-Infinity, 14]' + ); + }); + + it('rejects a start exclusive end inclusive data bin that does not fall into an end inclusive open start filter', () => { + const dataBin = { + start: 14, + end: 15, + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + '(14, 15] should be rejected by (-Infinity, 14]' + ); + }); + }); + + describe('test isDataBinSelected with a start exclusive open ended data bin for all filters', () => { + it('rejects a start exclusive open ended data bin for any categorical filter', () => { + const dataBin = { + start: 14, + specialValue: '>', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [categoryFilter]), + '(14, Infinity) should be rejected by Unknown' + ); + }); + + it('rejects a start exclusive open ended data bin for any single point filter', () => { + const dataBin = { + start: 13, + specialValue: '>', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [singlePointFilter]), + '(13, Infinity) should be rejected by 14' + ); + }); + + it('rejects a start exclusive open ended data bin for any start exclusive end inclusive filter', () => { + const dataBin = { + start: 14, + specialValue: '>', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [ + startExclusiveEndInclusiveFilter, + ]), + '(14, Infinity) should be rejected by (14, 15]' + ); + }); + + it('accepts a start exclusive open ended data bin that falls into a start exclusive open ended filter', () => { + const dataBin = { + start: 14, + specialValue: '>', + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + '(14, Infinity) should be accepted by (14, Infinity)' + ); + }); + + it('rejects a start exclusive open ended data bin that does not fall into a start exclusive open ended filter', () => { + const dataBin = { + start: 13, + specialValue: '>', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + '(13, Infinity) should be rejected by (14, Infinity)' + ); + }); + + it('accepts a start exclusive open ended data bin that falls into a start inclusive open ended filter', () => { + const dataBin = { + start: 14, + specialValue: '>', + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + '(14, Infinity) should be accepted by [14, Infinity)' + ); + }); + + it('rejects a start exclusive open ended data bin that does not fall into a start inclusive open ended filter', () => { + const dataBin = { + start: 13, + specialValue: '>', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + '(13, Infinity) should be rejected by [14, Infinity)' + ); + }); + + it('rejects a start exclusive open ended data bin for any end exclusive open start filter', () => { + const dataBin = { + start: 13, + specialValue: '>', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + '(13, Infinity) should be rejected by (-Infinity, 14)' + ); + }); + + it('rejects a start exclusive open ended data bin for any end inclusive open start filter', () => { + const dataBin = { + start: 13, + specialValue: '>', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + '(13, Infinity) should be rejected by (-Infinity, 14]' + ); + }); + }); + + describe('test isDataBinSelected with a start inclusive open ended data bin for all filters', () => { + it('rejects a start inclusive open ended data bin for any categorical filter', () => { + const dataBin = { + start: 14, + specialValue: '>=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [categoryFilter]), + '[14, Infinity) should be rejected by Unknown' + ); + }); + + it('rejects a start inclusive open ended data bin for any single point filter', () => { + const dataBin = { + start: 14, + specialValue: '>=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [singlePointFilter]), + '[14, Infinity) should be rejected by 14' + ); + }); + + it('rejects a start inclusive open ended data bin for any start exclusive end inclusive filter', () => { + const dataBin = { + start: 14, + specialValue: '>=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [ + startExclusiveEndInclusiveFilter, + ]), + '[14, Infinity) should be rejected by (14, 15]' + ); + }); + + it('rejects a start inclusive open ended data bin for any end exclusive open start filter', () => { + const dataBin = { + start: 13, + specialValue: '>=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + '[13, Infinity) should be rejected by (-Infinity, 14)' + ); + }); + + it('rejects a start inclusive open ended data bin for any end inclusive open start filter', () => { + const dataBin = { + start: 13, + specialValue: '>=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + '[13, Infinity) should be rejected by (-Infinity, 14]' + ); + }); + + it('accepts a start inclusive open ended data bin that falls into a start exclusive open ended filter', () => { + const dataBin = { + start: 15, + specialValue: '>=', + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + '[15, Infinity) should be accepted by (14, Infinity)' + ); + }); + + it('rejects a start inclusive open ended data bin that does not fall into a start exclusive open ended filter', () => { + const dataBin = { + start: 14, + specialValue: '>=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + '[14, Infinity) should be rejected by (14, Infinity)' + ); + }); + + it('accepts a start inclusive open ended data bin that falls into a start inclusive open ended filter', () => { + const dataBin = { + start: 14, + specialValue: '>=', + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + '[14, Infinity) should be accepted by [14, Infinity)' + ); + }); + + it('rejects a start inclusive open ended data bin that does not fall into a start inclusive open ended filter', () => { + const dataBin = { + start: 13, + specialValue: '>=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + '[13, Infinity) should be rejected by [14, Infinity)' + ); + }); + }); + + describe('test isDataBinSelected with an end exclusive open start data bin for all filters', () => { + it('rejects an end exclusive open start data bin for any categorical filter', () => { + const dataBin = { + start: 14, + specialValue: '<', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [categoryFilter]), + '(-Infinity, 14) should be rejected by Unknown' + ); + }); + + it('rejects an end exclusive open start data bin for any single point filter', () => { + const dataBin = { + end: 15, + specialValue: '<', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [singlePointFilter]), + '(-Infinity, 15) should be rejected by 14' + ); + }); + + it('rejects an end exclusive open start data bin for any start exclusive end inclusive filter', () => { + const dataBin = { + end: 15, + specialValue: '<', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [ + startExclusiveEndInclusiveFilter, + ]), + '(-Infinity, 15) should be rejected by (14, 15]' + ); + }); + + it('rejects an end exclusive open start data bin for any start exclusive open ended filter', () => { + const dataBin = { + end: 15, + specialValue: '<', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + '(-Infinity, 15) should be rejected by (14, Infinity)' + ); + }); + + it('rejects an end exclusive open start data bin for any start inclusive open ended filter', () => { + const dataBin = { + end: 14, + specialValue: '<', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + '(-Infinity, 14) should be rejected by [14, Infinity)' + ); + }); + + it('accepts an end exclusive open start data bin that falls into an end exclusive open start filter', () => { + const dataBin = { + end: 13, + specialValue: '<', + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + '(-Infinity, 13) should be accepted by (-Infinity, 14)' + ); + }); + + it('rejects an end exclusive open start data bin that does not fall into an end exclusive open start filter', () => { + const dataBin = { + end: 15, + specialValue: '<', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + '(-Infinity, 15) should be rejected by (-Infinity, 14)' + ); + }); + + it('accepts an end exclusive open start data bin that falls into an end inclusive open start filter', () => { + const dataBin = { + end: 14, + specialValue: '<', + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + '(-Infinity, 14) should be accepted by (-Infinity, 14]' + ); + }); + + it('rejects an end exclusive open start data bin that does not fall into an end inclusive open start filter', () => { + const dataBin = { + end: 15, + specialValue: '<', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + '(-Infinity, 15) should be rejected by (-Infinity, 14]' + ); + }); + }); + + describe('test isDataBinSelected with an end inclusive open start data bin for all filters', () => { + it('rejects an end inclusive open start data bin for any categorical filter', () => { + const dataBin = { + start: 14, + specialValue: '<=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [categoryFilter]), + '(-Infinity, 14] should be rejected by Unknown' + ); + }); + + it('rejects an end inclusive open start data bin for any single point filter', () => { + const dataBin = { + end: 14, + specialValue: '<=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [singlePointFilter]), + '(-Infinity, 14] should be rejected by 14' + ); + }); + + it('rejects an end inclusive open start data bin for any start exclusive end inclusive filter', () => { + const dataBin = { + end: 14, + specialValue: '<=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [ + startExclusiveEndInclusiveFilter, + ]), + '(-Infinity, 14] should be rejected by (14, 15]' + ); + }); + + it('rejects an end inclusive open start data bin for any start exclusive open ended filter', () => { + const dataBin = { + end: 14, + specialValue: '<=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + '(-Infinity, 14] should be rejected by (14, Infinity)' + ); + }); + + it('rejects an end inclusive open start data bin for any start inclusive open ended filter', () => { + const dataBin = { + end: 14, + specialValue: '<=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + '(-Infinity, 14] should be rejected by [14, Infinity)' + ); + }); + + it('accepts an end inclusive open start data bin that falls into an end exclusive open start filter', () => { + const dataBin = { + end: 13, + specialValue: '<=', + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + '(-Infinity, 13] should be accepted by (-Infinity, 14)' + ); + }); + + it('rejects an end inclusive open start data bin that does not fall into an end exclusive open start filter', () => { + const dataBin = { + end: 14, + specialValue: '<=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + '(-Infinity, 14] should be rejected by (-Infinity, 14)' + ); + }); + + it('accepts an end inclusive open start data bin that falls into an end inclusive open start filter', () => { + const dataBin = { + end: 14, + specialValue: '<=', + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + '(-Infinity, 14] should be accepted by (-Infinity, 14]' + ); + }); + + it('rejects an end inclusive open start data bin that does not fall into an end inclusive open start filter', () => { + const dataBin = { + end: 15, + specialValue: '<=', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + '(-Infinity, 15] should be rejected by (-Infinity, 14]' + ); + }); + }); + + describe('test isDataBinSelected with a categorical data bin for all filters', () => { + it('rejects a categorical data bin for any single point filter', () => { + const dataBin = { + specialValue: 'Unknown', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [singlePointFilter]), + 'Unknown should be rejected by 14' + ); + }); + + it('rejects a categorical data bin for any start exclusive end inclusive filter', () => { + const dataBin = { + specialValue: 'Unknown', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [ + startExclusiveEndInclusiveFilter, + ]), + 'Unknown should be rejected by (14, 15]' + ); + }); + + it('rejects a categorical data bin for any start exclusive open ended filter', () => { + const dataBin = { + specialValue: 'Unknown', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startExclusiveOpenEndedFilter]), + 'Unknown should be rejected by (14, Infinity)' + ); + }); + + it('rejects a categorical data bin for any start inclusive open ended filter', () => { + const dataBin = { + specialValue: 'Unknown', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [startInclusiveOpenEndedFilter]), + 'Unknown should be rejected by [14, Infinity)' + ); + }); + + it('rejects a categorical data bin for any end exclusive open start filter', () => { + const dataBin = { + specialValue: 'Unknown', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endExclusiveOpenStartFilter]), + 'Unknown should be rejected by (-Infinity, 14)' + ); + }); + + it('rejects a categorical data bin for any an end inclusive open start filter', () => { + const dataBin = { + specialValue: 'Unknown', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [endInclusiveOpenStartFilter]), + 'Unknown should be rejected by (-Infinity, 14]' + ); + }); + + it('accepts a categorical data bin that matches a categorical filter', () => { + const dataBin = { + specialValue: 'Unknown', + } as DataBin; + + assert.isTrue( + isDataBinSelected(dataBin, [categoryFilter]), + 'Unknown should be accepted by Unknown' + ); + }); + + it('rejects a categorical data bin that does not match a categorical filter', () => { + const dataBin = { + specialValue: 'Known', + } as DataBin; + + assert.isFalse( + isDataBinSelected(dataBin, [categoryFilter]), + 'Known should be rejected by Unknown' + ); + }); + }); + }); + describe('updateCustomIntervalFilter', () => { let newRange: { start?: number; end?: number }; let getDataBinsResult1 = observable.box([], { deep: false }); diff --git a/src/pages/studyView/StudyViewUtils.tsx b/src/pages/studyView/StudyViewUtils.tsx index b087e697fed..40ea892296e 100644 --- a/src/pages/studyView/StudyViewUtils.tsx +++ b/src/pages/studyView/StudyViewUtils.tsx @@ -1,37 +1,35 @@ import _ from 'lodash'; import { SingleGeneQuery } from 'shared/lib/oql/oql-parser'; -import { - ClinicalDataCount, - SampleIdentifier, - StudyViewFilter, - ClinicalDataBinFilter, - DataFilterValue, - GenomicDataBin, - GenomicDataCount, - GenericAssayDataMultipleStudyFilter, - GenericAssayData, - GeneFilterQuery, - DensityPlotBin, -} from 'cbioportal-ts-api-client'; import { CancerStudy, ClinicalAttribute, - Gene, - PatientIdentifier, - Sample, ClinicalData, + ClinicalDataBinFilter, + ClinicalDataCount, ClinicalDataMultiStudyFilter, - MolecularProfile, + DataFilterValue, + DensityPlotBin, + Gene, + GeneFilterQuery, GenePanelData, + GenericAssayData, + GenericAssayDataMultipleStudyFilter, + GenomicDataBin, + GenomicDataCount, MolecularDataMultipleStudyFilter, + MolecularProfile, NumericGeneMolecularData, + PatientIdentifier, + Sample, + SampleIdentifier, + StudyViewFilter, } from 'cbioportal-ts-api-client'; import * as React from 'react'; import { buildCBioPortalPageUrl } from '../../shared/api/urls'; import { BarDatum } from './charts/barChart/BarChart'; import { - GenomicChart, GenericAssayChart, + GenomicChart, XVsYChart, XVsYChartSettings, } from './StudyViewPageStore'; @@ -39,6 +37,7 @@ import { StudyViewPageTabKeyEnum } from 'pages/studyView/StudyViewPageTabs'; import { Layout } from 'react-grid-layout'; import internalClient from 'shared/api/cbioportalInternalClientInstance'; import defaultClient from 'shared/api/cbioportalClientInstance'; +import client from 'shared/api/cbioportalClientInstance'; import { ChartDimension, ChartTypeEnum, @@ -48,6 +47,8 @@ import { import { IStudyViewDensityScatterPlotDatum } from './charts/scatterPlot/StudyViewDensityScatterPlot'; import MobxPromise from 'mobxpromise'; import { + CNA_COLOR_AMP, + CNA_COLOR_HOMDEL, EditableSpan, getTextWidth, stringListToIndexSet, @@ -66,11 +67,10 @@ import { StructuralVariantProfilesEnum, } from 'shared/components/query/QueryStoreUtils'; import { - GenericAssayDataBin, ClinicalDataBin, + GenericAssayDataBin, } from 'cbioportal-ts-api-client/dist/generated/CBioPortalAPIInternal'; import { ChartOption } from './addChartButton/AddChartButton'; -import { CNA_COLOR_AMP, CNA_COLOR_HOMDEL } from 'cbioportal-frontend-commons'; import { observer } from 'mobx-react'; import { ChartUserSetting, @@ -78,8 +78,8 @@ import { VirtualStudy, } from 'shared/api/session-service/sessionServiceModels'; import { getServerConfig } from 'config/config'; -import client from 'shared/api/cbioportalClientInstance'; import joinJsx from 'shared/lib/joinJsx'; +import { BoundType, NumberRange } from 'range-ts'; // Cannot use ClinicalDataTypeEnum here for the strong type. The model in the type is not strongly typed export enum ClinicalDataTypeEnum { @@ -1218,6 +1218,74 @@ export function isEveryBinDistinct(data?: DataBin[]) { ); } +function createRangeForDataBinOrFilter( + start?: number, + end?: number, + specialValue?: string +): NumberRange { + if (start !== undefined && end !== undefined) { + if (start === end) { + return NumberRange.closed(start, end); // [start, end] + } else { + return NumberRange.openClosed(start, end); // (start, end] + } + } else if (start !== undefined && end === undefined) { + if (specialValue === '>=') { + return NumberRange.downTo(start, BoundType.CLOSED); // [start, Infinity) + } else { + return NumberRange.downTo(start, BoundType.OPEN); // (start, Infinity) + } + } else if (start === undefined && end !== undefined) { + if (specialValue === '<') { + return NumberRange.upTo(end, BoundType.OPEN); // (-Infinity, end) + } else { + return NumberRange.upTo(end, BoundType.CLOSED); // (-Infinity, end] + } + } else { + return NumberRange.all(); + } +} + +export function isDataBinSelected( + dataBin: DataBin, + filters: DataFilterValue[] +): boolean { + let isSelected: boolean; + + // numerical bin: + // the entire bin range (from bin.start to bin.end) should be enclosed by at least one of the filters + if (dataBin.start !== undefined || dataBin.end !== undefined) { + const numericalFilters = filters.filter( + filter => filter.start !== undefined || filter.end !== undefined + ); + isSelected = _.some(numericalFilters, filter => { + const filterRange = createRangeForDataBinOrFilter( + filter.start, + filter.end, + filter.value + ); + const binRange = createRangeForDataBinOrFilter( + dataBin.start, + dataBin.end, + dataBin.specialValue + ); + return filterRange.encloses(binRange); + }); + } + // categorical bin: + // there should be at least one filter with the same filter value + else { + const categoricalFilters = filters.filter( + filter => filter.start === undefined && filter.end === undefined + ); + isSelected = _.compact( + categoricalFilters.map(filter => filter.value) + ).includes(dataBin.specialValue); + } + + return isSelected; +} + export function isLogScaleByDataBins(data?: DataBin[]) { if (!data) { return false; diff --git a/src/pages/studyView/charts/barChart/BarChart.tsx b/src/pages/studyView/charts/barChart/BarChart.tsx index 6d0191b867a..5f932402b05 100644 --- a/src/pages/studyView/charts/barChart/BarChart.tsx +++ b/src/pages/studyView/charts/barChart/BarChart.tsx @@ -21,6 +21,7 @@ import { generateNumericalData, needAdditionShiftForLogScaleBarChart, DataBin, + isDataBinSelected, } from '../../StudyViewUtils'; import { STUDY_VIEW_CONFIG } from '../../StudyViewConfig'; import { DEFAULT_NA_COLOR } from 'shared/lib/Colors'; @@ -84,28 +85,6 @@ export default class BarChart extends React.Component this.props.onUserSelection(dataBins); } - private isDataBinSelected( - dataBin: DataBin, - filters: DataFilterValue[] - ): boolean { - return _.some(filters, filter => { - let isFiltered = false; - if (filter.start !== undefined && filter.end !== undefined) { - isFiltered = - filter.start <= dataBin.start && filter.end >= dataBin.end; - } else if (filter.start !== undefined && filter.end === undefined) { - isFiltered = dataBin.start >= filter.start; - } else if (filter.start === undefined && filter.end !== undefined) { - isFiltered = dataBin.end <= filter.end; - } else { - isFiltered = - filter.value !== undefined && - filter.value === dataBin.specialValue; - } - return isFiltered; - }); - } - public toSVGDOMNode(): Element { return this.svgContainer.firstChild; } @@ -370,7 +349,7 @@ export default class BarChart extends React.Component style={{ data: { fill: (d: BarDatum) => - this.isDataBinSelected( + isDataBinSelected( d.dataBin, this.props.filters ) || this.props.filters.length === 0 diff --git a/yarn.lock b/yarn.lock index 7e46e3b4f80..dce276102e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20295,6 +20295,11 @@ range-parser@~1.2.0: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= +range-ts@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/range-ts/-/range-ts-0.1.5.tgz#a0e6a18d32a4cd20cc2a0d48f7b95b983af940cf" + integrity sha512-U2Ksx7g/+MBb1eP8jbhma7/WkF5pFLRrbvTXx5VvRHkjhs2d/as27Zgv5uVt/m/QEEk0liY4CCwQzF22SRDknQ== + rat-vec@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/rat-vec/-/rat-vec-1.1.1.tgz#0dde2b66b7b34bb1bcd2a23805eac806d87fd17f"