Skip to content

Commit

Permalink
Merge pull request #4833 from TJMKuijpers/custom-interval-number-at-risk
Browse files Browse the repository at this point in the history
Updated Surivival Chart  for large cohort label overlap (number at risk)
  • Loading branch information
inodb committed Mar 7, 2024
2 parents 8325737 + 46ce32e commit 80440f5
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 31 deletions.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
167 changes: 136 additions & 31 deletions src/pages/resultsView/survival/SurvivalChart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { ReactNode } from 'react';
import { observer } from 'mobx-react';
import { PatientSurvival } from '../../../shared/model/PatientSurvival';
import { action, computed, observable, makeObservable } from 'mobx';
Expand Down Expand Up @@ -31,13 +32,15 @@ import {
SurvivalPlotFilters,
SurvivalSummary,
ScatterData,
calculateLabelWidth,
} from './SurvivalUtil';
import { toConditionalPrecision } from 'shared/lib/NumberUtils';
import { getPatientViewUrl } from '../../../shared/api/urls';
import {
DefaultTooltip,
DownloadControlOption,
DownloadControls,
setArrowLeft,
} from 'cbioportal-frontend-commons';
import autobind from 'autobind-decorator';
import { AnalysisGroup, DataBin } from '../../studyView/StudyViewUtils';
Expand All @@ -58,12 +61,12 @@ import {
} from 'pages/resultsView/survival/logRankTest';
import { getServerConfig } from 'config/config';
import LeftTruncationCheckbox from 'shared/components/survival/LeftTruncationCheckbox';
import * as victory from 'victory';
import { scaleLinear } from 'd3-scale';
import ReactSelect from 'react-select1';
import { categoryPlotTypeOptions } from 'pages/groupComparison/ClinicalData';
import SurvivalDescriptionTable from 'pages/resultsView/survival/SurvivalDescriptionTable';
import $ from 'jquery';
import SettingsMenu from 'shared/alterationFiltering/SettingsMenu';
export enum LegendLocation {
TOOLTIP = 'tooltip',
CHART = 'chart',
Expand All @@ -83,6 +86,12 @@ export type HazardInformationLegend = {
hazardInformation: string;
};

export type RiskPerGroup = {
groupName: string;
aliveSamples: number;
timePoint: number;
};

export interface LandmarkLineValues {
xStart: number;
xEnd: number;
Expand Down Expand Up @@ -298,6 +307,7 @@ export default class SurvivalChartExtended
this.props.analysisGroups.map((item: any) => item.name.length)
);
}

@computed
get downSamplingDenominators() {
return {
Expand Down Expand Up @@ -424,6 +434,7 @@ export default class SurvivalChartExtended
return null;
}
}

@computed get getOrderGroups() {
const selectedGroup = this.analysisGroupsWithData.filter(
item => item.legendText == this._controlGroup!.value
Expand Down Expand Up @@ -500,6 +511,7 @@ export default class SurvivalChartExtended
return null;
}
}

@action hazardRatioAtLandmark(threshold: number[]) {
const landmarkGroups: any = [];
const survivalData = this.props.sortedGroupedSurvivals;
Expand Down Expand Up @@ -739,6 +751,7 @@ export default class SurvivalChartExtended

return lines;
}

@computed get groupLandMarkLine() {
const landmarkLineLegend = this.analysisGroupsWithData.map(
(item: any) => item.name
Expand Down Expand Up @@ -863,50 +876,132 @@ export default class SurvivalChartExtended
);
return point;
}
@computed get numberOfSamplesAtRisk() {

@computed get timePointsForNumberAtRiskLabels() {
return scaleLinear()
.domain([0, this.sliderValue])
.ticks(18);
}

@computed get numberOfSamplesAtRisk(): ReactNode[] {
const orderOfLabels = this.analysisGroupsWithData.map(
item => item.value
);
const definedTimePoints: number[] = scaleLinear()
const timePoints: number[] = scaleLinear()
.domain([0, this.sliderValue])
.ticks(18);
const numberAtRisk = _.groupBy(
this.calculateGroupSize(definedTimePoints).sort(
this.calculateGroupSize(timePoints).sort(
(a, b) =>
orderOfLabels.indexOf(a.groupName) -
orderOfLabels.indexOf(b.groupName)
),
'timePoint'
);
const labelComponents: ReactNode[] = [];
let someTimePointsOverlap: boolean = false;

// Hide overlapping labels of necessary -> start with time point at index 0 and check
// if time point (index 0) and the second time point (index 1) overlap.
// If yes -> remove all labels at index 1,3,5, and so on
const checkOverlap = (
labelX: number,
labelWidth: number,
existingLabel: ReactNode
): boolean => {
const existingLabelX: number = (existingLabel as any).props.x; // Assuming VictoryLabel has an x attribute
const existingLabelWidth: number = calculateLabelWidth(
(existingLabel as any).props.text,
CBIOPORTAL_VICTORY_THEME.legend.style.labels.fontFamily,
CBIOPORTAL_VICTORY_THEME.legend.style.labels.fontSize
);

return Object.keys(numberAtRisk).map(item =>
numberAtRisk[item].map((grp, i) => {
return (
<VictoryLabel
text={numberAtRisk[item][i].aliveSamples}
x={
numberAtRisk[item][i].timePoint * this.scaleFactor +
this.styleOpts.padding.left
}
y={
this.styleOptsDefaultProps.height -
this.styleOpts.padding.bottom +
80 +
i * 20
}
style={{
fontFamily:
CBIOPORTAL_VICTORY_THEME.legend.style.labels
.fontFamily,
}}
textAnchor="middle"
/>
);
})
);
return !(
labelX + labelWidth < existingLabelX ||
labelX > existingLabelX + existingLabelWidth
);
};

const addLabelsForTimePoint = (
timePoint: number,
index: number
): void => {
const rowLabels: ReactNode[] = numberAtRisk[timePoint].map(
(grp, i) => {
const labelX: number =
grp.timePoint * this.scaleFactor +
this.styleOpts.padding.left;

const labelY: number =
this.styleOptsDefaultProps.height -
this.styleOpts.padding.bottom +
80 +
i * 20;

const labelWidth: number = calculateLabelWidth(
numberAtRisk[timePoint][i].aliveSamples,
CBIOPORTAL_VICTORY_THEME.legend.style.labels.fontFamily,
CBIOPORTAL_VICTORY_THEME.legend.style.labels.fontSize
);

const labelComponent: ReactNode = (
<VictoryLabel
key={`${timePoint}-${i}`}
text={numberAtRisk[timePoint][i].aliveSamples}
x={labelX}
y={labelY}
style={{
fontFamily:
CBIOPORTAL_VICTORY_THEME.legend.style.labels
.fontFamily,
}}
textAnchor="middle"
/>
);

labelComponents.push(labelComponent);
return labelComponent;
}
);
};
timePoints.forEach((timePoint, index) => {
const timePointOverlap: boolean = numberAtRisk[timePoint].some(
(grp, i) => {
const labelX: number =
grp.timePoint * this.scaleFactor +
this.styleOpts.padding.left;

const labelWidth: number = calculateLabelWidth(
numberAtRisk[timePoint][i].aliveSamples,
CBIOPORTAL_VICTORY_THEME.legend.style.labels.fontFamily,
CBIOPORTAL_VICTORY_THEME.legend.style.labels.fontSize
);

return labelComponents.some(existingLabel =>
checkOverlap(labelX, labelWidth, existingLabel)
);
}
);

if (!timePointOverlap) {
if (
!someTimePointsOverlap ||
(index % 2 === 0 && index + 1 !== timePoints.length - 1) ||
index === timePoints.length - 1
) {
addLabelsForTimePoint(timePoint, index);
}
} else {
someTimePointsOverlap = true;
}
});

return labelComponents;
}

@observable _latestLandMarkPoint: number = 0;
@action updatelatestLandMarkPoint(value: number) {

@action updateLatestLandMarkPoint(value: number) {
this._latestLandMarkPoint = value;
}

Expand Down Expand Up @@ -1005,6 +1100,7 @@ export default class SurvivalChartExtended
this.styleOpts.padding.right) /
value;
}

@observable _inputFieldVisible: boolean = false;
@observable _calculateHazardRatio: boolean = false;
@observable landmarkPoint: LandmarkLineValues[];
Expand All @@ -1021,12 +1117,15 @@ export default class SurvivalChartExtended
@observable hazardRatio: HazardRatioInformation[];
@observable labelOffset: number =
65 + (Object.keys(this.props.sortedGroupedSurvivals).length + 1) * 20;

@action openHooverBox() {
return (this.hooverBoxVisible = true);
}

@action closeHooverBox() {
return (this.hooverBoxVisible = false);
}

@action landmarkLinesChecked() {
if (!this._inputFieldVisible) {
return (this._inputFieldVisible = true);
Expand Down Expand Up @@ -1073,11 +1172,12 @@ export default class SurvivalChartExtended
yEnd: 103,
} as LandmarkLineValues)
);
this.updatelatestLandMarkPoint(landmarkArray[0].xStart);
this.updateLatestLandMarkPoint(landmarkArray[0].xStart);
this.updateVisibilityLandmarkLines();
this.calculateGroupSize(landmarkArray.map(obj => obj.xStart));
return (this.landmarkPoint = landmarkArray);
}

@action calculateHazardRatio() {
if (!this.showHazardRatio) {
this.showNormalLegend = false;
Expand Down Expand Up @@ -1115,6 +1215,7 @@ export default class SurvivalChartExtended
@action updateVisibilityLandmarkLines() {
return (this.showLandmarkLine = true);
}

@action.bound
onSliderTextChange(text: string) {
this.sliderValue = Number.parseFloat(text);
Expand All @@ -1124,13 +1225,16 @@ export default class SurvivalChartExtended
this.styleOpts.padding.right) /
Number.parseFloat(text);
}

@observable _controlGroup: { label: string; value: string } = {
label: this.availableGroups[0].label,
value: this.availableGroups[0].value,
};

@computed get selectedControlGroup() {
return this._controlGroup;
}

@computed get availableGroups() {
if (Object.keys(this.props.sortedGroupedSurvivals).length > 1) {
const filteredObjects = Object.keys(
Expand Down Expand Up @@ -1162,6 +1266,7 @@ export default class SurvivalChartExtended
];
}
}

@action.bound changeControlGroup(groupValue: {
label: string;
value: string;
Expand Down
17 changes: 17 additions & 0 deletions src/pages/resultsView/survival/SurvivalUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -738,3 +738,20 @@ export function calculateNumberOfPatients(
s.uniquePatientKey in patientToAnalysisGroups ? 1 : 0
);
}

export function calculateLabelWidth(
text: number,
fontFamily: string,
fontSize: number
) {
const tempElement = document.createElement('div');
tempElement.style.position = 'absolute';
tempElement.style.opacity = '0';
tempElement.style.fontFamily = fontFamily;
tempElement.style.fontSize = fontSize.toString();
tempElement.textContent = text.toString();
document.body.appendChild(tempElement);
const labelWidth = tempElement.offsetWidth;
document.body.removeChild(tempElement);
return labelWidth;
}

0 comments on commit 80440f5

Please sign in to comment.