Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pairwise plots #5041

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions src/shared/components/plots/BoxScatterPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
VictoryScatter,
VictoryLegend,
VictoryLabel,
VictoryLine,
} from 'victory';
import { IBaseScatterPlotData } from './ScatterPlot';
import {
Expand Down Expand Up @@ -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<D extends IBaseBoxScatterPlotPoint> {
Expand All @@ -56,6 +59,15 @@ export interface IBoxScatterPlotData<D extends IBaseBoxScatterPlotPoint> {
data: D[];
}

export type SampleIdsForPatientIds = {
[patientId: string]: string[];
};

export type CoordinatesForLinePlot = {
x: number;
y: number;
};

export interface IBoxScatterPlotProps<D extends IBaseBoxScatterPlotPoint> {
svgId?: string;
title?: string;
Expand Down Expand Up @@ -91,6 +103,8 @@ export interface IBoxScatterPlotProps<D extends IBaseBoxScatterPlotPoint> {
legendTitle?: string | string[];
pValue?: number | null;
qValue?: number | null;
renderLinePlot?: boolean;
samplesForPatients?: SampleIdsForPatientIds[] | [];
}

type BoxModel = {
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -662,6 +678,7 @@ export default class BoxScatterPlot<
Object.assign({}, d, {
[dataAxis]: d.value,
[categoryAxis]: categoryCoord,
// lineHovered: false,
} as { x: number; y: number })
);
}
Expand Down Expand Up @@ -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 (
<div
ref={this.containerRef}
Expand Down Expand Up @@ -833,6 +921,61 @@ export default class BoxScatterPlot<
horizontal={this.props.horizontal}
events={this.boxPlotEvents}
/>
{this.props.renderLinePlot &&
this.initLineVisibility &&
Object.keys(this.patientLinePlotData!).map(
patientId =>
this.visibleLines.get(patientId) && (
<VictoryLine
name="line"
key={patientId}
data={this.patientLinePlotData![patientId]}
x={this.scatterPlotX}
y={this.scatterPlotY}
style={{
data: {
stroke: 'grey',
strokeWidth: 2,
cursor: 'pointer',
pointerEvents: 'all',
},
}}
events={[
{
target: 'data',
eventHandlers: {
onMouseOver: () => {
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 => (
<VictoryScatter
key={`${dataWithAppearance.fill},${dataWithAppearance.stroke},${dataWithAppearance.strokeWidth},${dataWithAppearance.strokeOpacity},${dataWithAppearance.fillOpacity},${dataWithAppearance.symbol}`}
Expand Down
2 changes: 2 additions & 0 deletions src/shared/components/plots/PlotUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ export function scatterPlotSize(
return 8;
} else if (active) {
return 6;
// } else if (d.lineHovered) {
// return 6;
} else {
return 4;
}
Expand Down
88 changes: 88 additions & 0 deletions src/shared/components/plots/PlotsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ enum EventKey {
utilities_horizontalBars,
utilities_showRegressionLine,
utilities_viewLimitValues,
utilities_compareSamples, // event key to enable sample comparison
sortByMedian,
}

Expand Down Expand Up @@ -386,6 +387,11 @@ export type PlotsTabGeneOption = {
label: string; // hugo symbol
};

// Represents the sample IDs for each patient ID
export type SampleIdsForPatientIds = {
[patientId: string]: string[];
};

const searchInputTimeoutMs = 600;

class PlotsTabScatterPlot extends ScatterPlot<IScatterPlotData> {}
Expand Down Expand Up @@ -454,6 +460,8 @@ export default class PlotsTab extends React.Component<IPlotsTabProps, {}> {
@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 = '';
Expand Down Expand Up @@ -516,6 +524,63 @@ export default class PlotsTab extends React.Component<IPlotsTabProps, {}> {
}
}

// 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).
Expand Down Expand Up @@ -1770,6 +1835,9 @@ export default class PlotsTab extends React.Component<IPlotsTabProps, {}> {
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;
Expand Down Expand Up @@ -4522,6 +4590,10 @@ export default class PlotsTab extends React.Component<IPlotsTabProps, {}> {
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 &&
Expand Down Expand Up @@ -4563,6 +4635,20 @@ export default class PlotsTab extends React.Component<IPlotsTabProps, {}> {
)}
</div>
)}
{showCompareSamples && (
<div className="checkbox" style={{ marginTop: 14 }}>
<label>
<input
type="checkbox"
name="utilities_compareSamples"
value={EventKey.utilities_compareSamples}
checked={this.compareSamples}
onClick={this.onInputClick}
/>{' '}
Compare samples from the same patient
</label>
</div>
)}
{showDiscreteVsDiscreteOption && (
<div className="form-group">
<label>Plot Type</label>
Expand Down Expand Up @@ -5741,6 +5827,8 @@ export default class PlotsTab extends React.Component<IPlotsTabProps, {}> {
LEGEND_TO_BOTTOM_WIDTH_THRESHOLD
}
legendTitle={this.legendTitle}
renderLinePlot={this.compareSamples} // render line plot if checkbox is checked
samplesForPatients={this.samplesForEachPatient}
/>
);
break;
Expand Down
Loading