Skip to content

Commit

Permalink
✨ Add Time Range Selection to KM Plots on Study View
Browse files Browse the repository at this point in the history
  • Loading branch information
haynescd committed Jun 20, 2023
1 parent e9a08df commit e947235
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 26 deletions.
70 changes: 52 additions & 18 deletions src/pages/resultsView/survival/SurvivalChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
VictoryLabel,
VictoryScatter,
VictoryZoomContainer,
VictorySelectionContainer,
} from 'victory';
import {
getSurvivalSummaries,
Expand All @@ -29,12 +30,13 @@ import {
SurvivalPlotFilters,
SurvivalSummary,
SURVIVAL_COMPACT_MODE_THRESHOLD,
ScatterData,
} from './SurvivalUtil';
import { toConditionalPrecision } from 'shared/lib/NumberUtils';
import { getPatientViewUrl } from '../../../shared/api/urls';
import { DefaultTooltip, DownloadControls } from 'cbioportal-frontend-commons';
import autobind from 'autobind-decorator';
import { AnalysisGroup } from '../../studyView/StudyViewUtils';
import { AnalysisGroup, DataBin } from '../../studyView/StudyViewUtils';
import { AbstractChart } from '../../studyView/charts/ChartContainer';
import { toSvgDomNodeWithLegend } from '../../studyView/StudyViewUtils';
import classnames from 'classnames';
Expand Down Expand Up @@ -91,6 +93,8 @@ export interface ISurvivalChartProps {
xAxisTickCount?: number;
// Compact mode will hide censoring dots in the chart and do binning based on configuration
compactMode?: boolean;
attributeId?: string;
onUserSelection?: (dataBins: DataBin[]) => void;
}

const MIN_GROUP_SIZE_FOR_LOGRANK = 10;
Expand Down Expand Up @@ -594,6 +598,52 @@ export default class SurvivalChart
this.sliderValue = Number.parseFloat(text);
}

@autobind
private onSelection(data: any[]) {
const scatterPoints: Array<ScatterData> = data[1].data;
if (scatterPoints.length > 2) {
const dataBin = this.generateFilterDataBin(scatterPoints);
if (this.props.onUserSelection) {
this.props.onUserSelection([dataBin]);
}
console.log(dataBin);
}
}

private generateFilterDataBin(scatterPoints: Array<ScatterData>): DataBin {
const minX = scatterPoints[0].x;
const maxX = scatterPoints[scatterPoints.length - 1].x;
return {
id: this.props.attributeId!,
start: minX,
end: maxX,
} as DataBin;
}

@computed
get victoryChartContainer() {
return this.props.onUserSelection ? (
<VictorySelectionContainer
selectionDimension="x"
onSelection={this.onSelection}
/>
) : (
<VictoryZoomContainer
responsive={false}
disable={true}
zoomDomain={
this.props.showSlider
? { x: [0, this.sliderValue] }
: undefined
}
onZoomDomainChange={_.debounce((domain: any) => {
this.scatterFilter = domain as SurvivalPlotFilters;
}, 1000)}
containerRef={(ref: any) => (this.svgContainer = ref)}
/>
);
}

@computed
get chart() {
return (
Expand Down Expand Up @@ -676,23 +726,7 @@ export default class SurvivalChart
</div>

<VictoryChart
containerComponent={
<VictoryZoomContainer
responsive={false}
disable={true}
zoomDomain={
this.props.showSlider
? { x: [0, this.sliderValue] }
: undefined
}
onZoomDomainChange={_.debounce((domain: any) => {
this.scatterFilter = domain as SurvivalPlotFilters;
}, 1000)}
containerRef={(ref: any) =>
(this.svgContainer = ref)
}
/>
}
containerComponent={this.victoryChartContainer}
height={this.styleOpts.height}
width={this.styleOpts.width}
padding={this.styleOpts.padding}
Expand Down
16 changes: 12 additions & 4 deletions src/pages/studyView/StudyViewPageStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ import {
StudyViewFilterQueryExtractor,
StudyViewQueryExtractor,
} from './StudyViewQueryExtractor';
import {
getAllowedSurvivalClinicalDataFilterId,
isSurvivalAttributeId,
isSurvivalChart,
} from './charts/survival/StudyViewSurvivalUtils';

export const STUDY_VIEW_FILTER_AUTOSUBMIT = 'study_view_filter_autosubmit';

Expand Down Expand Up @@ -2772,10 +2777,12 @@ export class StudyViewPageStore
if (this.chartMetaSet[chartUniqueKey]) {
let chartMeta = this.chartMetaSet[chartUniqueKey];
trackStudyViewFilterEvent('clinicalDataFilters', this);
this.updateClinicalAttributeFilterByValues(
chartMeta.clinicalAttribute!.clinicalAttributeId,
values
);

const attributeId: string = isSurvivalChart(chartMeta.uniqueKey)
? getAllowedSurvivalClinicalDataFilterId(chartMeta.uniqueKey)
: chartMeta.clinicalAttribute!.clinicalAttributeId;

this.updateClinicalAttributeFilterByValues(attributeId, values);
}
}
@action.bound
Expand Down Expand Up @@ -6026,6 +6033,7 @@ export class StudyViewPageStore
dataType: getChartMetaDataType(survivalPlot.id),
patientAttribute: true,
displayName: survivalPlot.title,
clinicalAttribute: survivalPlot.survivalStatusAttribute,
// use survival status attribute's priority as KM plot's priority for non-reserved plots
priority:
STUDY_VIEW_CONFIG.priority[survivalPlot.id] ||
Expand Down
11 changes: 9 additions & 2 deletions src/pages/studyView/UserSelections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ import {
import { StudyViewPageStore } from 'pages/studyView/StudyViewPageStore';
import classNames from 'classnames';
import { StudyViewPageTabKeyEnum } from 'pages/studyView/StudyViewPageTabs';
import {
getSurvivalChartMetaId,
isSurvivalAttributeId,
} from './charts/survival/StudyViewSurvivalUtils';

export interface IUserSelectionsProps {
store: StudyViewPageStore;
Expand Down Expand Up @@ -547,9 +551,12 @@ export default class UserSelections extends React.Component<
return _.reduce(
filters || [],
(acc, clinicalDataFilter) => {
const chartMeta = this.props.attributesMetaSet[
const attributeId = isSurvivalAttributeId(
clinicalDataFilter.attributeId
];
)
? getSurvivalChartMetaId(clinicalDataFilter.attributeId)
: clinicalDataFilter.attributeId;
const chartMeta = this.props.attributesMetaSet[attributeId];
if (chartMeta) {
const dataType = this.props.clinicalAttributeIdToDataType[
clinicalDataFilter.attributeId
Expand Down
5 changes: 4 additions & 1 deletion src/pages/studyView/charts/ChartContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,8 @@ export class ChartContainer extends React.Component<IChartContainerProps, {}> {
this.props.chartMeta.uniqueKey
]?.survivalData,
this.props.analysisGroupsSettings.groups,
this.props.patientToAnalysisGroup!.result!
this.props.patientToAnalysisGroup!.result!,
this.props.chartMeta.uniqueKey
);
} else {
return undefined;
Expand Down Expand Up @@ -981,6 +982,8 @@ export class ChartContainer extends React.Component<IChartContainerProps, {}> {
yAxisTickCount={2}
xAxisTickCount={4}
compactMode={this.showCompactSurvivalChart}
attributeId={data.attributeId}
onUserSelection={this.handlers.onDataBinSelection}
/>
);
} else {
Expand Down
33 changes: 32 additions & 1 deletion src/pages/studyView/charts/survival/StudyViewSurvivalUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { sortPatientSurvivals } from 'pages/resultsView/survival/SurvivalUtil';
export function makeSurvivalChartData(
patientSurvivals: ReadonlyArray<PatientSurvival>,
analysisGroups: ReadonlyArray<AnalysisGroup>,
patientToAnalysisGroup: { [uniquePatientKey: string]: string }
patientToAnalysisGroup: {
[uniquePatientKey: string]: string;
},
attributeId: string
) {
let patientToAnalysisGroups = _.mapValues(patientToAnalysisGroup, group => [
group,
Expand Down Expand Up @@ -48,7 +51,35 @@ export function makeSurvivalChartData(
analysisGroups,
sortedGroupedSurvivals,
pValue,
attributeId,
};
}

export function makeScatterPlotData() {}

export function isSurvivalAttributeId(attributeId: string) {
return attributeId.includes('_MONTHS') || attributeId.includes('_STATUS');
}

export function isSurvivalChart(chartUniqueKey: string) {
return chartUniqueKey.includes('_SURVIVAL');
}

export function getAllowedSurvivalClinicalDataFilterId(chartUniqueKey: string) {
const prefix = chartUniqueKey.substring(
0,
chartUniqueKey.indexOf('_SURVIVAL')
);
return `${prefix}_MONTHS`;
}

export function getSurvivalChartMetaId(attributeId: string) {
const survivalClinicalDataType = attributeId.includes('_MONTHS')
? '_MONTHS'
: '_STATUS';
const prefix = attributeId.substring(
0,
attributeId.indexOf(survivalClinicalDataType)
);
return `${prefix}_SURVIVAL`;
}
1 change: 1 addition & 0 deletions src/pages/studyView/tabs/SummaryTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ export class StudySummaryTab extends React.Component<
'OS_SURVIVAL'
]?.survivalDataWithoutLeftTruncation;
}
props.onDataBinSelection = this.handlers.onDataBinSelection;
/* end of left truncation adjustment related settings */
break;
}
Expand Down

0 comments on commit e947235

Please sign in to comment.