From 079f3422a35cd083f93e0c9a0c77fa6035c386c6 Mon Sep 17 00:00:00 2001 From: Sowmiyaa Date: Fri, 25 Oct 2024 11:20:24 +0200 Subject: [PATCH] Pairwise comparison plots --- .../components/plots/BoxScatterPlot.tsx | 143 ++++++++++++++++++ src/shared/components/plots/PlotUtils.ts | 2 + src/shared/components/plots/PlotsTab.tsx | 88 +++++++++++ 3 files changed, 233 insertions(+) diff --git a/src/shared/components/plots/BoxScatterPlot.tsx b/src/shared/components/plots/BoxScatterPlot.tsx index 0681bf22e1a..b4a549c49c1 100644 --- a/src/shared/components/plots/BoxScatterPlot.tsx +++ b/src/shared/components/plots/BoxScatterPlot.tsx @@ -11,6 +11,7 @@ import { VictoryScatter, VictoryLegend, VictoryLabel, + VictoryLine, } from 'victory'; import { IBaseScatterPlotData } from './ScatterPlot'; import { @@ -48,6 +49,8 @@ import { PQValueLabel } from 'pages/groupComparison/MultipleCategoryBarPlot'; export interface IBaseBoxScatterPlotPoint { value: number; jitter?: number; // between -1 and 1 + sampleId: string; + // lineHovered?: boolean; } export interface IBoxScatterPlotData { @@ -56,6 +59,15 @@ export interface IBoxScatterPlotData { data: D[]; } +export type SampleIdsForPatientIds = { + [patientId: string]: string[]; +}; + +export type CoordinatesForLinePlot = { + x: number; + y: number; +}; + export interface IBoxScatterPlotProps { svgId?: string; title?: string; @@ -91,6 +103,8 @@ export interface IBoxScatterPlotProps { legendTitle?: string | string[]; pValue?: number | null; qValue?: number | null; + renderLinePlot?: boolean; + samplesForPatients?: SampleIdsForPatientIds[] | []; } type BoxModel = { @@ -130,6 +144,8 @@ export default class BoxScatterPlot< @observable.ref private container: HTMLDivElement; @observable.ref private boxPlotTooltipModel: any | null; @observable private mousePosition = { x: 0, y: 0 }; + @observable visibleLines = new Map(); + @observable removingLines: boolean = false; private scatterPlotTooltipHelper: ScatterPlotTooltipHelper = new ScatterPlotTooltipHelper(); @@ -662,6 +678,7 @@ export default class BoxScatterPlot< Object.assign({}, d, { [dataAxis]: d.value, [categoryAxis]: categoryCoord, + // lineHovered: false, } as { x: number; y: number }) ); } @@ -779,8 +796,79 @@ export default class BoxScatterPlot< } } + // called everytime page refreshes + @computed get patientLinePlotData() { + const patientDataForLinePlot: { [patientId: string]: any[] } = {}; + + if (this.props.renderLinePlot && this.props.samplesForPatients) { + this.props.samplesForPatients.forEach(patientObject => { + Object.keys(patientObject).forEach(patientId => { + const sampleIds: string[] = patientObject[patientId]; + + patientDataForLinePlot[patientId] = []; + + this.scatterPlotData.forEach(datawithAppearance => { + datawithAppearance.data.forEach(sampleArray => { + if (sampleIds.includes(sampleArray.sampleId)) { + patientDataForLinePlot[patientId].push(sampleArray); + } + }); + }); + }); + }); + } + return patientDataForLinePlot; + } + + // to populate the very first time (or everytime the page refreshes) + @bind + private initLineVisibility() { + this.updateRemovingLines; + if (this.patientLinePlotData && this.props.renderLinePlot) { + Object.keys(this.patientLinePlotData).forEach(patientId => { + if (!this.visibleLines.has(patientId)) { + this.visibleLines.set(patientId, true); + } + // on re-checking the checkbox, all patientIds should be set to true + if (this.visibleLines.has(patientId) && !this.removingLines) { + this.visibleLines.set(patientId, true); + } + }); + } + } + + @action.bound + private toggleLineVisibility(patientId: string) { + this.removingLines = true; + this.visibleLines.set(patientId, false); + } + + @computed get updateRemovingLines() { + if (!this.props.renderLinePlot) { + this.removingLines = false; + } + return null; + } + + // @action.bound + // isLineHovered(data: any, hovered: boolean) { + // console.log(this.scatterPlotData); + // data.forEach((sampleArray: any) => { + // this.scatterPlotData.map(dataWithAppearance => { + // dataWithAppearance.data.map(sample => { + // if (sample.sampleId === sampleArray.sampleId) { + // sample.lineHovered = hovered; // TODO: gets updated when clicked but not when hovered + // console.log(`Sample ${sample.sampleId} lineHovered: ${sample.lineHovered}`); + // console.log(sample); + // } + // }) + // }) + // }) + // } + @autobind private getChart() { + this.initLineVisibility(); return (
+ {this.props.renderLinePlot && + this.initLineVisibility && + Object.keys(this.patientLinePlotData!).map( + patientId => + this.visibleLines.get(patientId) && ( + { + return [ + { + target: 'data', + mutation: () => { + // this.isLineHovered(this.patientLinePlotData![patientId], true); + return { style: { stroke: 'black', strokeWidth: 3 } } + } + } + ] + }, + onMouseOut: () => { + return [ + { + target: 'data', + mutation: () => { + // this.isLineHovered(this.patientLinePlotData![patientId], false); + return { style: { stroke: 'grey', strokeWidth: 2 } } + } + } + ]; + }, + onClick: () => { + this.toggleLineVisibility(patientId); + return []; + }, + }, + }, + ]} + /> + ) + )} {this.scatterPlotData.map(dataWithAppearance => ( {} @@ -454,6 +460,8 @@ export default class PlotsTab extends React.Component { @observable percentageBar = false; @observable stackedBar = false; @observable viewLimitValues: boolean = true; + // an observable boolean for comparing samples - initialize to false + @observable compareSamples: boolean = false; @observable _waterfallPlotSortOrder: string | undefined = undefined; @observable searchCase: string = ''; @@ -516,6 +524,63 @@ export default class PlotsTab extends React.Component { } } + // Return an array of the patient IDs of the data points in the box plot - will use as an identifier for each line + @computed get patientIdsInBoxPlot(): string[] { + let patientIds: string[] = []; + + if (this.boxPlotData.isComplete && this.boxPlotData.result) { + const uniqueSampleKeys = _.flatten( + _.map(this.boxPlotData.result.data, dataPoint => + _.map(dataPoint.data, point => point.uniqueSampleKey) + ) + ); + + patientIds = _.uniq( + uniqueSampleKeys + .map( + sampleKey => + this.props.sampleKeyToSample.result![sampleKey] + ?.patientId + ) + .filter(Boolean) + ); + } + return patientIds; + } + + @computed get samplesForEachPatient(): SampleIdsForPatientIds[] { + const samplesForPatients: SampleIdsForPatientIds[] = []; + + // for each patient ID in the box plot, get the sample IDs for that patient + // initialize an object with the patient ID as the key and an empty array as the value + if (this.patientIdsInBoxPlot && this.patientIdsInBoxPlot.length > 0) { + this.patientIdsInBoxPlot.forEach(patientId => { + const sampleIdsForPatient: SampleIdsForPatientIds = { + [patientId]: [], + }; + + this.boxPlotData.result?.data.forEach(dataPoint => { + dataPoint.data.forEach(point => { + const sample = this.props.sampleKeyToSample.result![ + point.uniqueSampleKey + ]; + if (sample && sample.patientId === patientId) { + sampleIdsForPatient[patientId].push(point.sampleId); + } + }); + }); + samplesForPatients.push(sampleIdsForPatient); + }); + } + // if atleast one patient has multiple samples, return the array of sample IDs for each patient, otherwise [] + const hasPatientWithMultipleSamples = samplesForPatients.some( + patientObject => + patientObject[Object.keys(patientObject)[0]].length > 1 + ); + + return hasPatientWithMultipleSamples ? samplesForPatients : []; + } + // determine whether formatting for points in the scatter plot (based on // mutations type, CNA, ...) will actually be shown in the plot (depends // on user choice via check boxes). @@ -1770,6 +1835,9 @@ export default class PlotsTab extends React.Component { case EventKey.utilities_viewLimitValues: this.viewLimitValues = !this.viewLimitValues; break; + case EventKey.utilities_compareSamples: + this.compareSamples = !this.compareSamples; + break; case EventKey.sortByMedian: this.boxPlotSortByMedian = !this.boxPlotSortByMedian; break; @@ -4522,6 +4590,10 @@ export default class PlotsTab extends React.Component { const showRegression = this.plotType.isComplete && this.plotType.result === PlotType.ScatterPlot; + const showCompareSamples = + this.plotType.isComplete && + this.plotType.result == PlotType.BoxPlot && + this.samplesForEachPatient.length > 0; if ( !showSearchOptions && !showSampleColoringOptions && @@ -4563,6 +4635,20 @@ export default class PlotsTab extends React.Component { )}
)} + {showCompareSamples && ( +
+ +
+ )} {showDiscreteVsDiscreteOption && (
@@ -5741,6 +5827,8 @@ export default class PlotsTab extends React.Component { LEGEND_TO_BOTTOM_WIDTH_THRESHOLD } legendTitle={this.legendTitle} + renderLinePlot={this.compareSamples} // render line plot if checkbox is checked + samplesForPatients={this.samplesForEachPatient} /> ); break;