diff --git a/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_dbs_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_dbs_element_chrome_1600x1000.png index d2db35c62fe..3959effa534 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_dbs_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_dbs_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_id_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_id_element_chrome_1600x1000.png index cdb19986aea..2f99205d104 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_id_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_id_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_sbs_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_sbs_element_chrome_1600x1000.png index cdb19986aea..2f99205d104 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_sbs_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_mutational_bar_chart_sbs_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/show_mutational_signatures_table_for_patient_who_has_significant_id_signatures_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_mutational_signatures_table_for_patient_who_has_significant_id_signatures_element_chrome_1600x1000.png index cdb19986aea..cdd56447de7 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_mutational_signatures_table_for_patient_who_has_significant_id_signatures_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_mutational_signatures_table_for_patient_who_has_significant_id_signatures_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/show_mutational_signatures_table_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_mutational_signatures_table_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png index d108ee0cd52..cce96517dca 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_mutational_signatures_table_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_mutational_signatures_table_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/show_stacked_bar_chart_for_patient_who_has_significant_dbs_signatures_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_stacked_bar_chart_for_patient_who_has_significant_dbs_signatures_element_chrome_1600x1000.png index a4998620024..453a52fa97a 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_stacked_bar_chart_for_patient_who_has_significant_dbs_signatures_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_stacked_bar_chart_for_patient_who_has_significant_dbs_signatures_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/show_stacked_bar_chart_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_stacked_bar_chart_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png index 25a1b029dde..ded09abb56e 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_stacked_bar_chart_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_stacked_bar_chart_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/show_the_bar_chart_with_percentage_on_y_axis_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_the_bar_chart_with_percentage_on_y_axis_element_chrome_1600x1000.png index e9d91513fb5..aa521f883f6 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_the_bar_chart_with_percentage_on_y_axis_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_the_bar_chart_with_percentage_on_y_axis_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_dbs_signatures_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_dbs_signatures_element_chrome_1600x1000.png index 09ea1983225..cd0c384d863 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_dbs_signatures_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_dbs_signatures_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_id_signatures_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_id_signatures_element_chrome_1600x1000.png index dd64e389730..eff611afd80 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_id_signatures_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_id_signatures_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png index d5b27ea985a..596f1b50cf3 100644 Binary files a/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/show_tooltip_for_patient_who_has_significant_sbs_signatures_element_chrome_1600x1000.png differ diff --git a/src/pages/patientView/PatientViewPageTabs.tsx b/src/pages/patientView/PatientViewPageTabs.tsx index de3b0049381..f19e798ce7e 100644 --- a/src/pages/patientView/PatientViewPageTabs.tsx +++ b/src/pages/patientView/PatientViewPageTabs.tsx @@ -602,7 +602,7 @@ export function tabs( diff --git a/src/pages/patientView/clinicalInformation/ClinicalInformationMutationalSignatureTable.spec.tsx b/src/pages/patientView/clinicalInformation/ClinicalInformationMutationalSignatureTable.spec.tsx index 2b0a80c6eca..190d8ed5d6a 100644 --- a/src/pages/patientView/clinicalInformation/ClinicalInformationMutationalSignatureTable.spec.tsx +++ b/src/pages/patientView/clinicalInformation/ClinicalInformationMutationalSignatureTable.spec.tsx @@ -1,7 +1,7 @@ import * as ClinicalInformationMutationalSignatureTable from './ClinicalInformationMutationalSignatureTable'; import React from 'react'; import { assert } from 'chai'; -import { prepareMutationalSignatureDataForTable } from './ClinicalInformationMutationalSignatureTable'; +import { prepareMutationalSignatureDataForTable } from '../mutationalSignatures/MutationalSignatureBarChartUtils'; import { IMutationalSignature } from 'shared/model/MutationalSignature'; const sampleMutationalSignatureMeta = [ @@ -84,7 +84,7 @@ describe('ClinicalInformationMutationalSignatureTable', () => { it('takes mutational signature sample data and formats it for mutational signature table to render', () => { let result = prepareMutationalSignatureDataForTable( sampleMutationalSignatureData, - samples + ['firstSample', 'secondSample'] ); assert.deepEqual(result, [ { diff --git a/src/pages/patientView/clinicalInformation/ClinicalInformationMutationalSignatureTable.tsx b/src/pages/patientView/clinicalInformation/ClinicalInformationMutationalSignatureTable.tsx index a6491d9059e..c47d005a625 100644 --- a/src/pages/patientView/clinicalInformation/ClinicalInformationMutationalSignatureTable.tsx +++ b/src/pages/patientView/clinicalInformation/ClinicalInformationMutationalSignatureTable.tsx @@ -4,27 +4,28 @@ import LazyMobXTable, { } from 'shared/components/lazyMobXTable/LazyMobXTable'; import styles from './style/mutationalSignatureTable.module.scss'; import { SHOW_ALL_PAGE_SIZE } from '../../../shared/components/paginationControls/PaginationControls'; -import { IMutationalSignature } from '../../../shared/model/MutationalSignature'; import { getMutationalSignaturePercentage } from '../../../shared/lib/FormatUtils'; import _ from 'lodash'; import { observer } from 'mobx-react'; import { action, computed, makeObservable, observable } from 'mobx'; import { MUTATIONAL_SIGNATURES_SIGNIFICANT_PVALUE_THRESHOLD } from 'shared/lib/GenericAssayUtils/MutationalSignaturesUtils'; -import { DownloadControlOption } from 'cbioportal-frontend-commons'; -import { getServerConfig } from 'config/config'; import Tooltip from 'rc-tooltip'; - +import { ILazyMobXTableApplicationDataStore } from 'shared/lib/ILazyMobXTableApplicationDataStore'; export interface IClinicalInformationMutationalSignatureTableProps { - data: IMutationalSignature[]; - parentCallback: (childData: string, visibility: boolean) => void; + data: IMutationalSignatureRow[]; url: string; signature: string; description: string; + samples: string[]; + onRowClick: (d: IMutationalSignatureRow) => void; + onRowMouseEnter?: (d: IMutationalSignatureRow) => void; + onRowMouseLeave?: (d: IMutationalSignatureRow) => void; + dataStore: ILazyMobXTableApplicationDataStore; } class MutationalSignatureTable extends LazyMobXTable {} -interface IMutationalSignatureRow { +export interface IMutationalSignatureRow { name: string; sampleValues: { [sampleId: string]: { @@ -36,55 +37,13 @@ interface IMutationalSignatureRow { url: string; } -export function prepareMutationalSignatureDataForTable( - mutationalSignatureData: IMutationalSignature[], - samples: { id: string }[] -): IMutationalSignatureRow[] { - const tableData: IMutationalSignatureRow[] = []; - //group data by mutational signature - //[{id: mutationalsignatureid, samples: [{}, {}]}] - let sampleInvertedDataByMutationalSignature: Array = _( - mutationalSignatureData - ) - .groupBy( - mutationalSignatureSample => mutationalSignatureSample.meta.name - ) - .map((mutationalSignatureSampleData, name) => ({ - name, - samples: mutationalSignatureSampleData, - url: mutationalSignatureSampleData[0].meta.url, - })) - .value(); - for (const mutationalSignature of sampleInvertedDataByMutationalSignature) { - let mutationalSignatureRowForTable: IMutationalSignatureRow = { - name: '', - sampleValues: {}, - url: '', - }; - mutationalSignatureRowForTable.name = mutationalSignature.name; - mutationalSignatureRowForTable.url = mutationalSignature.url; - for (const sample of mutationalSignature.samples) { - mutationalSignatureRowForTable.sampleValues[sample.sampleId] = { - value: sample.value, - confidence: sample.confidence, - }; - } - if ( - Object.keys(mutationalSignatureRowForTable.sampleValues).length === - samples.length - ) { - tableData.push(mutationalSignatureRowForTable); - } - } - return tableData; -} @observer export default class ClinicalInformationMutationalSignatureTable extends React.Component< IClinicalInformationMutationalSignatureTableProps, {} > { - @observable selectedSignature = ''; - + @observable + selectedSignature: string; constructor(props: IClinicalInformationMutationalSignatureTableProps) { super(props); makeObservable(this); @@ -96,40 +55,33 @@ export default class ClinicalInformationMutationalSignatureTable extends React.C this.selectedSignature = e.currentTarget.innerHTML; } @computed get uniqueSamples() { - return _.map(_.uniqBy(this.props.data, 'sampleId'), uniqSample => ({ - id: uniqSample.sampleId, - })); - } - - @computed get tableData() { - return prepareMutationalSignatureDataForTable( - this.props.data, - this.uniqueSamples + return _.map( + this.props.data.map(x => Object.keys(x.sampleValues))[0], + uniqSample => ({ + id: uniqSample, + }) ); } + @computed get tooltipInfo() { return (
-

- Signature: +

+ Signature: {this.props.signature} -
-

+ +

Description: {this.props.description}

-

- {this.props.url != '' && ( - - External link to signature (opens new tab) - - )} - {this.props.url == '' && - 'No link to external website available'} +

+ + External link to signature (opens new tab) +

@@ -141,17 +93,13 @@ export default class ClinicalInformationMutationalSignatureTable extends React.C { name: 'Mutational Signature', render: (data: IMutationalSignatureRow) => ( - - { - - this.props.parentCallback(data.name, false) - } - > - {data[this.firstCol]} - - } - + + {data[this.firstCol]} + {' '} + + {} + + ), download: (data: IMutationalSignatureRow) => `${data[this.firstCol]}`, @@ -174,20 +122,23 @@ export default class ClinicalInformationMutationalSignatureTable extends React.C MUTATIONAL_SIGNATURES_SIGNIFICANT_PVALUE_THRESHOLD ? ( //if it's a significant signature, bold the contribution // Based on significant pvalue the span is created with style.mutationalSignatureValue for bold (sign) // or normal styling (not signficant) - + {getMutationalSignaturePercentage( data.sampleValues[col.id].value )} ) : ( - + {getMutationalSignaturePercentage( data.sampleValues[col.id].value )} ) ) : ( - + {getMutationalSignaturePercentage( data.sampleValues[col.id].value )} @@ -209,25 +160,27 @@ export default class ClinicalInformationMutationalSignatureTable extends React.C .indexOf(filterStringUpper) > -1, sortBy: (data: IMutationalSignatureRow) => data.sampleValues[col.id].value, + align: 'right' as 'right', })), ]; } public render() { return ( - +
+ +
); } } diff --git a/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts b/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts index 16b9508461b..bcaabf1ee5f 100644 --- a/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts +++ b/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts @@ -913,6 +913,15 @@ export class PatientViewPageStore { .filter((value, index, self) => self.indexOf(value) === index); } + @computed get samplesNotProfiledForMutationalSignatures(): string[] { + const allSamples = this.samplesWithUniqueKeys.result.map( + sample => sample.sampleId + ); + return allSamples.filter( + element => !this.samplesWithCountDataAvailable.includes(element) + ); + } + readonly samplesWithoutCancerTypeClinicalData = remoteData( { await: () => [this.samples, this.clinicalDataForSamples], diff --git a/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChart.spec.tsx b/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChart.spec.tsx index 1421fcb9128..ec00079172a 100644 --- a/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChart.spec.tsx +++ b/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChart.spec.tsx @@ -82,7 +82,7 @@ const sampleMutationalSignatureData: IMutationalCounts[] = [ describe('MutationalSignatureBarChart', () => { it('Takes unsorted IMutationalCounts[] and transforms it to sorted IColorDataChart', () => { - let result = getColorsForSignatures(sampleMutationalSignatureData); + let result = getColorsForSignatures(sampleMutationalSignatureData, '%'); assert.deepEqual(result, [ { uniqueSampleKey: 's09e3B34', @@ -97,6 +97,7 @@ describe('MutationalSignatureBarChart', () => { colorValue: 'red', label: 'A[C>T]G', subcategory: ' ', + sublabel: ' ', group: 'C>T', }, { @@ -112,6 +113,7 @@ describe('MutationalSignatureBarChart', () => { colorValue: 'red', label: 'A[C>T]G', subcategory: ' ', + sublabel: ' ', group: 'C>T', }, @@ -125,8 +127,9 @@ describe('MutationalSignatureBarChart', () => { mutationalSignatureClass: 'T>A', version: 'v2', value: 20, - colorValue: 'grey', + colorValue: '#99A3A4', subcategory: ' ', + sublabel: ' ', label: 'A[T>A]G', group: 'T>A', }, @@ -134,7 +137,8 @@ describe('MutationalSignatureBarChart', () => { }); it('Takes unsorted IMutationalCounts[] and transforms it to unsorted IColorDataChart', () => { let result = getColorsForSignatures( - sampleMutationalSignatureDataWithoutClass + sampleMutationalSignatureDataWithoutClass, + '%' ); assert.deepEqual(result, [ { @@ -149,6 +153,7 @@ describe('MutationalSignatureBarChart', () => { colorValue: 'red', label: 'A[C>T]G', subcategory: ' ', + sublabel: ' ', mutationalSignatureClass: '', group: 'C>T', }, @@ -164,6 +169,7 @@ describe('MutationalSignatureBarChart', () => { colorValue: 'red', label: 'A[C>T]G', subcategory: ' ', + sublabel: ' ', mutationalSignatureClass: '', group: 'C>T', }, @@ -180,6 +186,7 @@ describe('MutationalSignatureBarChart', () => { colorValue: 'red', label: 'A[C>T]G', subcategory: ' ', + sublabel: ' ', mutationalSignatureClass: '', group: 'C>T', }, diff --git a/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChart.tsx b/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChart.tsx index ec4d0f94c43..abf3e82d97a 100644 --- a/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChart.tsx +++ b/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChart.tsx @@ -1,39 +1,29 @@ import * as React from 'react'; -import { - VictoryBar, - VictoryAxis, - VictoryChart, - VictoryLabel, - VictoryTooltip, -} from 'victory'; -import { action, computed } from 'mobx'; -import { observer } from 'mobx-react'; -import WindowStore from 'shared/components/window/WindowStore'; - import _ from 'lodash'; +import { VictoryBar, VictoryAxis, VictoryLabel, VictoryTooltip } from 'victory'; +import { action, computed, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; import { IMutationalCounts } from 'shared/model/MutationalSignature'; import { getColorsForSignatures, - ColorMapProps, - colorMap, IColorDataBar, - IColorLegend, getLegendEntriesBarChart, getxScalePoint, - LegendLabelsType, DrawRectInfo, LabelInfo, getLengthLabelEntries, createLegendLabelObjects, formatLegendObjectsForRectangles, getCenterPositionLabelEntries, + DataToPlot, + addColorsForReferenceData, } from './MutationalSignatureBarChartUtils'; import { CBIOPORTAL_VICTORY_THEME } from 'cbioportal-frontend-commons'; export interface IMutationalBarChartProps { signature: string; - width: number; height: number; + width: number; refStatus: boolean; svgId: string; svgRef?: (svgContainer: SVGElement | null) => void; @@ -41,6 +31,9 @@ export interface IMutationalBarChartProps { version: string; sample: string; label: string; + selectedScale: string; + initialReference: string; + updateReference: boolean; } const theme = _.cloneDeep(CBIOPORTAL_VICTORY_THEME); @@ -50,7 +43,19 @@ theme.legend.style.data = { strokeWidth: 0, stroke: 'black', }; +type FrequencyData = { channel: string; frequency: number }; + +const cosmicReferenceData = require('./cosmic_reference.json'); +const offSetYAxis = 45; +const heightYAxis = 300; +const replaceLabels = [ + 'Homopolymer length', + 'Homopolymer length', + 'Number of repeat units', + 'Number of repeat units', + 'Microhomology', +]; @observer export default class MutationalBarChart extends React.Component< IMutationalBarChartProps, @@ -58,29 +63,50 @@ export default class MutationalBarChart extends React.Component< > { constructor(props: IMutationalBarChartProps) { super(props); + reaction( + () => this.props.width, + newWidth => { + this.graphWidth = newWidth; + } + ); } + @observable svgWidth = this.props.width; + @observable graphWidth = this.props.width - 50; @computed get xTickLabels(): string[] { - return getColorsForSignatures(this.props.data).map(item => item.label); + return getColorsForSignatures( + this.props.data, + this.props.selectedScale + ).map(item => item.label); } @computed get yAxisDomain(): number[] { - const maxValue = this.props.data.reduce( - (previous: IMutationalCounts, current: IMutationalCounts) => { - return current.value > previous.value ? current : previous; - } - ); - const minValue = this.props.data.reduce( - (previous: IMutationalCounts, current: IMutationalCounts) => { - return current.value < previous.value ? current : previous; + if (this.props.selectedScale == '%') { + return [0, 100]; + } else { + const maxValue = this.props.data.reduce( + (previous: IMutationalCounts, current: IMutationalCounts) => { + return current.value > previous.value ? current : previous; + } + ); + + if (maxValue.value !== 0) { + return [0, Math.round(maxValue.value)]; + } else { + return [0, 10]; } - ); - return [minValue.value, maxValue.value + 0.1 * maxValue.value]; + } } @computed get getGroupedData() { if (this.props.data[0].mutationalSignatureLabel != '') { - return _.groupBy(getColorsForSignatures(this.props.data), 'group'); + return _.groupBy( + getColorsForSignatures( + this.props.data, + this.props.selectedScale + ), + 'group' + ); } else { return this.props.data; } @@ -90,29 +116,13 @@ export default class MutationalBarChart extends React.Component< } @computed get formatLegendTopAxisPoints() { - const labelObjects = getColorsForSignatures(this.props.data).map( - entry => ({ - group: entry.group, - label: entry.label, - color: entry.colorValue, - subcategory: entry.subcategory, - }) - ); - const legendObjects = getLegendEntriesBarChart(labelObjects); - const lengthLegendObjects = getCenterPositionLabelEntries( - legendObjects - ); const legendOjbectsToAdd = createLegendLabelObjects( - lengthLegendObjects, - legendObjects, + this.centerPositionLabelEntries, + this.getLegendObjects, this.getMutationalSignaturesGroupLabels ); - const centerOfBoxes = this.formatColorBoxLegend; - const xScale = getxScalePoint( - labelObjects, - 60, - WindowStore.size.width - 90 - ); + const centerOfBoxes = this.colorRectangles; + const xScale = this.getXScale; return legendOjbectsToAdd.map((item, i) => { return ( ); }); } - @computed get formatColorBoxLegend() { - const legendLabels = getColorsForSignatures(this.props.data).map( - entry => ({ - group: entry.group, - label: entry.label, - color: entry.colorValue, - subcategory: entry.subcategory, - }) - ); - const xScale = getxScalePoint( - legendLabels, - 60, - WindowStore.size.width - 90 + @computed get labelObjects() { + return getColorsForSignatures( + this.props.data, + this.props.selectedScale + ).map(entry => ({ + group: entry.group, + label: entry.mutationalSignatureLabel, + color: entry.colorValue, + subcategory: entry.subcategory, + value: entry.label, + })); + } + + @computed get legendInfo() { + const centerPositionLabelEntries = getLengthLabelEntries( + this.getLegendObjects ); - const legendEntries = getLegendEntriesBarChart(legendLabels); - const lengthLegendObjects = getLengthLabelEntries(legendEntries); - const legendInfoBoxes = formatLegendObjectsForRectangles( - lengthLegendObjects, - legendEntries, + return formatLegendObjectsForRectangles( + centerPositionLabelEntries, + this.getLegendObjects, this.getMutationalSignaturesGroupLabels, - this.props.version + this.props.version, + 'subcategory' ); + } + + @computed get getXScale() { + return getxScalePoint(this.labelObjects, 65, this.graphWidth - 45); + } + + @computed get colorRectangles() { + const legendInfoBoxes = this.legendInfo; const legendRectsChart: JSX.Element[] = []; + const xScale = this.getXScale; legendInfoBoxes.forEach((item: DrawRectInfo) => { legendRectsChart.push( 0 - ? xScale(item.end)! - xScale(item.start)! - : 6 + ? xScale(item.end)! - xScale(item.start)! + 12 + : 15 } - height="15" + height="20" /> ); }); return legendRectsChart; } - @computed get getSubLabelsLegend() { - const groupedData = _.groupBy( - getColorsForSignatures(this.props.data), - g => g.group - ); - const legendLabels = getColorsForSignatures(this.props.data).map( - entry => ({ - group: entry.group, - label: entry.label, - color: entry.colorValue, - subcategory: entry.subcategory, - value: entry.label, - }) - ); - const uniqueSubLabels = legendLabels.filter( - (value, index, self) => - index === - self.findIndex( - t => - t.group === value.group && - t.subcategory === value.subcategory - ) - ); + @computed get uniqueLabelsForRectangles() { + return this.labelObjects + .filter( + (value, index, self) => + index === + self.findIndex( + t => + t.group === value.group && + t.subcategory === value.subcategory + ) + ) + .map(item => { + if ( + item.group === '>1bp deletion' || + item.group === '>1bp insertion' || + item.group === 'Microhomology' + ) { + item.subcategory == '5' + ? (item.subcategory = '5+') + : item.subcategory; + } + return item; + }); + } - const centerOfBoxes = this.formatColorBoxLegend; + @computed get getSubLabelsForRectangles() { + const coloredBoxes = this.colorRectangles; const subLabelsForBoxes = formatLegendObjectsForRectangles( - [uniqueSubLabels.length], - uniqueSubLabels, - uniqueSubLabels.map(item => item.subcategory!), - this.props.version + [this.uniqueLabelsForRectangles.length], + this.uniqueLabelsForRectangles, + this.uniqueLabelsForRectangles.map(item => item.subcategory!), + this.props.version, + 'subcategory' ); const legendLabelsChart: JSX.Element[] = []; - subLabelsForBoxes.forEach((item: LabelInfo, i: number) => { + subLabelsForBoxes.forEach((item: LabelInfo) => { legendLabelsChart.push( x.props.fill === item.color)[0] + .props.x + + 0.5 * + coloredBoxes.filter( + x => x.props.fill === item.color + )[0].props.width } - y={37} - width={this.props.width} + y={25} + width={this.graphWidth} text={item.category} - style={{ fontSize: '10px' }} + style={{ fontSize: 14, fontWeight: 'bold' }} textAnchor={'middle'} /> ); @@ -223,49 +256,326 @@ export default class MutationalBarChart extends React.Component< return legendLabelsChart; } - @action getLabelsForTooltip(data: IMutationalCounts[]): string[] { - return getColorsForSignatures(data).map(item => item.label); + @computed get getLegendObjects() { + return getLegendEntriesBarChart(this.labelObjects); + } + + @computed get centerPositionLabelEntries() { + return getCenterPositionLabelEntries(this.getLegendObjects); + } + + @computed get legendObjectsToAdd() { + return createLegendLabelObjects( + this.centerPositionLabelEntries, + this.getLegendObjects, + this.getMutationalSignaturesGroupLabels + ); + } + + @computed get xAxisLabelsInDel() { + const lengthLegendObjects = getCenterPositionLabelEntries( + this.getLegendObjects + ); + const legendOjbectsToAdd = createLegendLabelObjects( + lengthLegendObjects, + this.getLegendObjects, + this.getMutationalSignaturesGroupLabels + ); + const xScale = this.getXScale; + return legendOjbectsToAdd.map((item, i) => { + return ( + + ); + }); + } + + @computed get formatLabelsCosmicStyle(): string[] { + const labels = this.getLabels(this.props.data); + const cosmicLabel: string[] = []; + if (this.props.version == 'SBS') { + labels.map(label => { + const labelSplit = label + .split('_') + .map((x, i) => { + return i == 1 ? x.split('-')[0] : x; + }) + .join(''); + cosmicLabel.push(labelSplit); + }); + } else if (this.props.version == 'DBS') { + labels.map(label => { + cosmicLabel.push(label.split('-')[1]); + }); + } else if (this.props.version == 'ID') { + labels.map(label => { + const labelSplit = label.split('_'); + if (labelSplit.includes('Ins')) { + labelSplit[3] == '5' + ? cosmicLabel.push('6+') + : cosmicLabel.push( + (Number(labelSplit[3]) + 1).toString() + ); + } else { + cosmicLabel.push(labelSplit[3]); + } + }); + } + return cosmicLabel; + } + + @computed get getReferenceSignatureToPlot() { + const currentSignature: string = + typeof this.props.signature !== 'undefined' + ? this.props.signature.split(' ')[0] + : this.props.initialReference.split(' ')[0]; + const referenceSignatureToPlot: FrequencyData[] = + cosmicReferenceData['v3.3']['GRCh37'][this.props.version][ + currentSignature + ]; + const referenceData: DataToPlot[] = referenceSignatureToPlot.map( + (sig: FrequencyData) => { + return { + mutationalSignatureLabel: sig.channel, + value: -1 * (sig.frequency * 100), + }; + } + ); + const referenceSorted = this.sortReferenceSignatures(referenceData); + return addColorsForReferenceData(referenceSorted); + } + + @computed get colorBoxXAxis() { + const legendLabels = getColorsForSignatures( + this.props.data, + this.props.selectedScale + ).map(entry => ({ + group: entry.group, + label: entry.mutationalSignatureLabel, + color: entry.colorValue, + subcategory: entry.subcategory, + })); + const xScale = this.getXScale; + const legendEntries = getLegendEntriesBarChart(legendLabels); + const lengthLegendObjects = getLengthLabelEntries(legendEntries); + const legendInfoBoxes = formatLegendObjectsForRectangles( + lengthLegendObjects, + legendEntries, + this.getMutationalSignaturesGroupLabels, + this.props.version, + 'subcategory' + ); + const legendRectsChart: JSX.Element[] = []; + legendInfoBoxes.forEach((item: DrawRectInfo, index: number) => { + legendRectsChart.push( + 0 + ? xScale(item.end)! - xScale(item.start)! + 12 + : 15 + } + height="20" + /> + ); + }); + return legendRectsChart; + } + + @action getLabels(data: IMutationalCounts[]): string[] { + return getColorsForSignatures(data, this.props.selectedScale).map( + item => item.mutationalSignatureLabel + ); + } + @action.bound + private renderCustomTickLabel = (tickProps: any) => { + const { x, y, index, text } = tickProps; + const secondLetter = text.charAt(1); + const colors: string[] = this.labelObjects.map(item => item.color); + const coloredText = + this.props.version === 'SBS' && + (secondLetter === 'C' || secondLetter === 'T') ? ( + {secondLetter} + ) : ( + {secondLetter} + ); + + return ( + + {text.charAt(0)} + {coloredText} + {text.substring(2)} + + ); + }; + + @computed get labelOrder() { + return getColorsForSignatures( + this.props.data, + this.props.selectedScale + ).map(item => item.mutationalSignatureLabel, this.props.selectedScale); + } + + @action sortReferenceSignatures(referenceData: DataToPlot[]) { + const labelsOrder = getColorsForSignatures( + this.props.data, + this.props.selectedScale + ).map(item => item.mutationalSignatureLabel, this.props.selectedScale); + const referenceOrder = referenceData.map( + (itemReference: any) => itemReference.mutationalSignatureLabel + ); + if (_.isEqual(labelsOrder, referenceOrder)) { + return referenceData; + } else { + const sorted = referenceData.sort( + (a: DataToPlot, b: DataToPlot) => { + return ( + labelsOrder.findIndex( + p => p === a.mutationalSignatureLabel + ) - + labelsOrder.findIndex( + p => p === b.mutationalSignatureLabel + ) + ); + } + ); + return sorted; + } + } + + @computed get referenceAxisLabel() { + const referenceString = `COSMIC Reference`; + return this.props.version === 'SBS' + ? referenceString + '\n' + this.props.signature + ' (%)' + : this.props.version === 'DBS' + ? referenceString + '\n' + this.props.signature + ' (%)' + : referenceString + '\n' + this.props.signature + ' (%)'; + } + + @action getTranslateDistance(defaultValue: number): number { + return this.props.version == 'SBS' + ? defaultValue - 10 + : this.props.version == 'DBS' + ? defaultValue - 15 + : defaultValue - 25; } public render() { return ( -
+
{this.formatLegendTopAxisPoints} - {this.formatColorBoxLegend} - {this.props.version == 'ID' && this.getSubLabelsLegend} - - + + Number.isInteger(t) ? t.toFixed(0) : '' + } height={300} + width={this.graphWidth + 45} + offsetX={offSetYAxis} + style={{ + paddingTop: 20, + paddingLeft: 20, + paddingRight: 20, + axis: { strokeWidth: 1 }, + axisLabel: { + fontFamily: + theme.bar.style.labels.fontFamily, + padding: + this.props.selectedScale == '%' + ? 35 + : 40, + letterSpacing: 'normal', + }, + ticks: { size: 5, stroke: 'black' }, + tickLabels: { + fontFamily: + theme.bar.style.labels.fontFamily, + padding: 2, + }, + grid: { + stroke: 'lightgrey', + strokeWidth: 0.3, + strokeDasharray: 10.5, + }, + }} + standalone={false} + /> + + {this.props.updateReference && ( + + + )} + + } + standalone={false} + /> + + + + } + alignment="middle" + data={getColorsForSignatures( + this.props.data, + this.props.selectedScale + )} + x="mutationalSignatureLabel" + y="value" + style={{ + paddingTop: 30, + paddingLeft: 30, + paddingRight: 30, + fontFamily: theme.bar.style.labels.fontFamily, + data: { + fill: (d: IColorDataBar) => d.colorValue, + }, + }} + standalone={false} + /> + + {!this.props.updateReference && ( + + + Select a signature from the table to show the + reference signature plot + + + )} + {this.props.version == 'ID' && this.colorBoxXAxis} + {this.props.version == 'ID' && this.xAxisLabelsInDel} + {this.props.updateReference && ( + + d.colorValue, + }, + }} + alignment="middle" + labels={this.formatLabelsCosmicStyle} labelComponent={ } - data={getColorsForSignatures(this.props.data)} - x="label" - y="value" - style={{ - data: { - fill: (d: IColorDataBar) => - d.colorValue, - }, - }} - alignment="start" - standalone={false} - /> - 6 - ? 55 - : 40, - angle: 270, - textAnchor: 'start', - verticalAnchor: 'middle', - }, - axis: { strokeWidth: 1 }, - grid: { stroke: 0 }, - }} standalone={false} /> - - + + )}
); diff --git a/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChartUtils.ts b/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChartUtils.ts index 65bbdc224a3..9afbba54c6e 100644 --- a/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChartUtils.ts +++ b/src/pages/patientView/mutationalSignatures/MutationalSignatureBarChartUtils.ts @@ -1,6 +1,10 @@ import _ from 'lodash'; -import { IMutationalCounts } from 'shared/model/MutationalSignature'; -import { scalePoint, scaleBand } from 'd3-scale'; +import { + IMutationalCounts, + IMutationalSignature, +} from 'shared/model/MutationalSignature'; +import { scalePoint } from 'd3-scale'; +import { IMutationalSignatureRow } from 'pages/patientView/clinicalInformation/ClinicalInformationMutationalSignatureTable'; export interface IColorLegend extends IColorDataBar { group: string; subcategory?: string; @@ -10,6 +14,7 @@ export interface IColorDataBar extends IMutationalCounts { colorValue: string; label: string; subcategory?: string; + sublabel?: string; } export interface ColorMapProps { @@ -18,6 +23,7 @@ export interface ColorMapProps { category: string; color: string; subcategory?: string; + sublabel?: string; } export interface LegendLabelsType { @@ -25,6 +31,7 @@ export interface LegendLabelsType { label: string; color: string; subcategory?: string; + sublabel?: string; } export interface DrawRectInfo { color: string; @@ -37,6 +44,7 @@ export interface LabelInfo { end: string; category: string; group: string; + sublabel: string; } export interface LegendEntriesType { group: string; @@ -45,23 +53,40 @@ export interface LegendEntriesType { value: string; } +export type DataToPlot = { mutationalSignatureLabel: string; value: number }; + export const colorMap: ColorMapProps[] = [ { name: 'C>A', alternativeName: '_C-A_', category: 'C>A', - color: 'lightblue', + color: '#5DADE2', }, { name: 'C>G', alternativeName: '_C-G_', category: 'C>G', - color: 'darkblue', + color: '#154360', }, { name: 'C>T', alternativeName: '_C-T_', category: 'C>T', color: 'red' }, - { name: 'T>A', alternativeName: '_T-A_', category: 'T>A', color: 'grey' }, - { name: 'T>C', alternativeName: '_T-C_', category: 'T>C', color: 'green' }, - { name: 'T>G', alternativeName: '_T-G_', category: 'T>G', color: 'pink' }, + { + name: 'T>A', + alternativeName: '_T-A_', + category: 'T>A', + color: '#99A3A4', + }, + { + name: 'T>C', + alternativeName: '_T-C_', + category: 'T>C', + color: '#2ECC71 ', + }, + { + name: 'T>G', + alternativeName: '_T-G_', + category: 'T>G', + color: '#F5B7B1', + }, { name: 'reference', alternativeName: 'reference', @@ -139,6 +164,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '1_Del_C_', category: '1bp deletion', subcategory: 'C', + sublabel: 'Homopolymer length', color: '#f39c12', }, { @@ -146,6 +172,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '1_Del_T_', category: '1bp deletion', subcategory: 'T', + sublabel: 'Homopolymer length', color: '#d68910', }, { @@ -153,6 +180,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '2_Del_R_', category: '>1bp deletion', subcategory: '2', + sublabel: 'Number of Repeat Units', color: '#f1948a', }, { @@ -160,6 +188,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '2_Del_M', category: 'Microhomology', subcategory: '2', + sublabel: 'Microhomology length', color: '#D2B7F2', }, { @@ -167,6 +196,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '3_Del_R', category: '>1bp deletion', subcategory: '3', + sublabel: 'Number of Repeat Units', color: '#ec7063', }, { @@ -174,13 +204,15 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '3_Del_M', category: 'Microhomology', subcategory: '3', - color: '#9b59b6', + sublabel: 'Microhomology length', + color: '#E194EB', }, { name: '4:Del:R', alternativeName: '4_Del_R', category: '>1bp deletion', subcategory: '4', + sublabel: 'Number of Repeat Units', color: '#e74c3c', }, { @@ -188,27 +220,31 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '4_Del_M', category: 'Microhomology', subcategory: '4', - color: '#7d3c98', + sublabel: 'Microhomology length', + color: '#DD75EA', }, { name: '5:Del:R', alternativeName: '5_Del_R', category: '>1bp deletion', subcategory: '5', - color: '#cb4335', + sublabel: 'Number of Repeat Units', + color: '#F7406C', }, { name: '5:Del:M', alternativeName: '5_Del_M', category: 'Microhomology', subcategory: '5', - color: '#4a235a', + sublabel: 'Microhomology length', + color: '#DB3AEE', }, { name: '1:Ins:T', alternativeName: '1_Ins_T', category: '1bp insertion', subcategory: 'T', + sublabel: 'Homopolymer length', color: '#28b463', }, { @@ -216,6 +252,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '1_Ins_C', category: '1bp insertion', subcategory: 'C', + sublabel: 'Homopolymer length', color: '#82e0aa', }, { @@ -223,6 +260,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '2_Ins_M', category: 'Microhomology', subcategory: '2', + sublabel: 'Microhomology length', color: '#aed6f1', }, { @@ -230,6 +268,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '2_Ins_R', category: '>1bp insertion', subcategory: '2', + sublabel: 'Number of Repeat Units', color: '#33ffff', }, { @@ -237,6 +276,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '3_Ins_M', category: 'Microhomology', subcategory: '3', + sublabel: 'Microhomology length', color: '#85c1e9', }, { @@ -244,6 +284,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '3_Ins_R', category: '>1bp insertion', subcategory: '3', + sublabel: 'Number of Repeat Units', color: '#aed6F1', }, { @@ -251,6 +292,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '4_Ins_M', category: 'Microhomology', subcategory: '4', + sublabel: 'Microhomology length', color: '#85c1e9', }, { @@ -258,6 +300,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '4_Ins_R', category: '>1bp insertion', subcategory: '4', + sublabel: 'Number of Repeat Units', color: '#5dade2', }, { @@ -265,6 +308,7 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '5_Ins_M', category: 'Microhomology', subcategory: '5', + sublabel: 'Microhomology length', color: '#3498db', }, { @@ -272,11 +316,13 @@ export const colorMap: ColorMapProps[] = [ alternativeName: '5_Ins_R', category: '>1bp insertion', subcategory: '5', - color: '#2874a6', + sublabel: 'Number of Repeat Units', + color: '#368BFD', }, ]; export function getColorsForSignatures( - dataset: IMutationalCounts[] + dataset: IMutationalCounts[], + yAxisSetting: string ): IColorLegend[] { const colorTableData = dataset.map((obj: IMutationalCounts) => { if (obj.mutationalSignatureLabel !== '') { @@ -285,20 +331,28 @@ export function getColorsForSignatures( obj.mutationalSignatureLabel.indexOf('_') == -1 && obj.mutationalSignatureLabel.indexOf('-') == -1 ) { - if (obj.mutationalSignatureLabel.match(cmap.name) != null) { + if ( + obj.mutationalSignatureLabel.match(cmap.name) !== null + ) { return cmap.color; } } else { if ( obj.mutationalSignatureLabel.match( cmap.alternativeName - ) != null + ) !== null ) { return cmap.color; } } }); - const label = obj.mutationalSignatureLabel; + const label = formatTooltipLabelCosmicStyle( + obj.version, + obj.mutationalSignatureLabel, + colorIdentity, + yAxisSetting, + obj.value + ); const group: string = colorIdentity.length > 0 ? colorIdentity[colorIdentity.length - 1].category @@ -311,18 +365,22 @@ export function getColorsForSignatures( 'subcategory' in colorIdentity[colorIdentity.length - 1] ? colorIdentity[colorIdentity.length - 1].subcategory! : ' '; - return { ...obj, colorValue, label, subcategory, group }; + const sublabel: string = + 'sublabel' in colorIdentity[colorIdentity.length - 1] + ? colorIdentity[colorIdentity.length - 1].sublabel! + : ' '; + return { ...obj, colorValue, label, subcategory, sublabel, group }; } else { const label = obj.mutationalSignatureLabel; const colorValue = '#EE4B2B'; const group = ' '; const subcategory: string = ' '; - return { ...obj, colorValue, label, subcategory, group }; + const sublabel: string = ''; + return { ...obj, colorValue, label, subcategory, sublabel, group }; } }); if (colorTableData[0].group !== ' ') { - const colorTableDataSorted = _.sortBy(colorTableData, 'group'); - return colorTableDataSorted; + return _.sortBy(colorTableData, 'group'); } else { return colorTableData; } @@ -343,20 +401,21 @@ export function getPercentageOfMutationalCount( mutationalSignatureLabel: item.mutationalSignatureLabel, mutationalSignatureClass: item.mutationalSignatureClass, version: item.version, - value: count, + value: sumValue == 0 ? 0 : count, }; }); } export function getxScalePoint( labels: LegendLabelsType[], - xmin: number, - xmax: number + xMin: number, + xMax: number ) { - return scaleBand() + return scalePoint() .domain(labels.map((x: LegendLabelsType) => x.label)) - .range([xmin, xmax]); + .range([xMin, xMax]); } + export function getLegendEntriesBarChart( labels: LegendLabelsType[] ): LegendEntriesType[] { @@ -369,6 +428,35 @@ export function getLegendEntriesBarChart( })); } +export function addColorsForReferenceData(dataset: DataToPlot[]) { + const colors = dataset.map((entry: DataToPlot) => { + const colorIdentity = colorMap.filter(cmap => { + if ( + entry.mutationalSignatureLabel.indexOf('_') == -1 && + entry.mutationalSignatureLabel.indexOf('-') == -1 + ) { + if (entry.mutationalSignatureLabel.match(cmap.name) != null) { + return cmap.color; + } + } else { + if ( + entry.mutationalSignatureLabel.match( + cmap.alternativeName + ) != null + ) { + return cmap.color; + } + } + }); + const colorValue = + colorIdentity.length > 0 + ? colorIdentity[colorIdentity.length - 1].color + : '#EE4B2B'; + return { ...entry, colorValue }; + }); + return colors; +} + export function getCenterPositionLabelEntries( legendObjects: LegendEntriesType[] ): number[] { @@ -395,22 +483,33 @@ export function createLegendLabelObjects( ); } +export function createXAxisAnnotation( + lengthObjects: number[], + objects: LegendEntriesType[], + labels: string[] +) { + return labels.map( + (identifier: string, i: number) => + _.groupBy(objects, 'sublabel')[identifier][lengthObjects[i] - 1] + ); +} + export function formatLegendObjectsForRectangles( lengthLegendObjects: number[], legendEntries: LegendEntriesType[], labels: string[], - version: string + version: string, + groupByString: string ) { if (version != 'ID') { - const formatLegendRect = lengthLegendObjects.map((value, i) => ({ + return lengthLegendObjects.map((value, i) => ({ color: _.groupBy(legendEntries, 'group')[labels[i]][0].color, start: _.groupBy(legendEntries, 'group')[labels[i]][0].value, end: _.groupBy(legendEntries, 'group')[labels[i]][value - 1].value, })); - return formatLegendRect; } else { // Create a new object grouped by 'group' and 'subcategory - const dataGroupByCategory = _.groupBy(legendEntries, 'subcategory'); + const dataGroupByCategory = _.groupBy(legendEntries, groupByString); const dataGroupByGroup = Object.keys(dataGroupByCategory).map(item => _.groupBy(dataGroupByCategory[item], 'group') ); @@ -418,7 +517,7 @@ export function formatLegendObjectsForRectangles( dataGroupByGroup.map(item => Object.keys(item).map(x => result.push(item[x])) ); - const formatLegendRect = result.map(itemLegend => ({ + return result.map(itemLegend => ({ color: itemLegend[0].color, start: itemLegend[0].label, end: @@ -428,6 +527,243 @@ export function formatLegendObjectsForRectangles( category: itemLegend[0].subcategory, group: itemLegend[0].group, })); - return formatLegendRect; } } + +export function prepareMutationalSignatureDataForTable( + mutationalSignatureData: IMutationalSignature[], + samplesInData: string[] +): IMutationalSignatureRow[] { + const tableData: IMutationalSignatureRow[] = []; + const sampleInvertedDataByMutationalSignature: Array = _( + mutationalSignatureData + ) + .groupBy( + mutationalSignatureSample => mutationalSignatureSample.meta.name + ) + .map((mutationalSignatureSampleData, name) => ({ + name, + samples: mutationalSignatureSampleData, + url: mutationalSignatureSampleData[0].meta.url, + })) + .value(); + for (const mutationalSignature of sampleInvertedDataByMutationalSignature) { + let mutationalSignatureRowForTable: IMutationalSignatureRow = { + name: '', + sampleValues: {}, + url: '', + }; + + mutationalSignatureRowForTable.name = mutationalSignature.name; + mutationalSignatureRowForTable.url = mutationalSignature.url; + if ( + Object.keys(mutationalSignature.samples).length === + samplesInData.length + ) { + for (const sample of mutationalSignature.samples) { + mutationalSignatureRowForTable.sampleValues[sample.sampleId] = { + value: sample.value, + confidence: sample.confidence, + }; + } + tableData.push(mutationalSignatureRowForTable); + } else { + for (const sampleId of samplesInData) { + if ( + mutationalSignature.samples.some( + (obj: IMutationalSignature) => obj.sampleId === sampleId + ) + ) { + // Sample exists and we can use the values + for (const sample of mutationalSignature.samples) { + mutationalSignatureRowForTable.sampleValues[ + sample.sampleId + ] = { + value: sample.value, + confidence: sample.confidence, + }; + } + } else { + mutationalSignatureRowForTable.sampleValues[sampleId] = { + value: 0, + confidence: 1, + }; + } + } + tableData.push(mutationalSignatureRowForTable); + } + } + return tableData; +} + +export function formatTooltipLabelCosmicStyle( + version: string, + label: string, + category: ColorMapProps[], + yAxisSetting: string, + value: number +): string { + const valueLabel = yAxisSetting == '%' ? '%' : ' count(#)'; + if (version == 'SBS') { + const labelSplit = label.split('_').map((x, i) => { + return i == 1 ? '[' + x.replace('-', '->') + ']' : x; + }); + return labelSplit.length > 1 + ? value + + valueLabel + + ' of SBS mutations (' + + percentageToFraction(value) + + ') are ' + + '\n' + + labelSplit[1] + + ' at ' + + labelSplit.join('') + + ' trinucleotide' + : label; + } else if (version == 'DBS') { + const labelSplit = label.split('-'); + return labelSplit.length > 1 + ? value + + valueLabel + + ' of DBS mutations (' + + percentageToFraction(value) + + ') ' + + '\n' + + labelSplit[0] + + ' to ' + + labelSplit[1] + : label; + } else if (version == 'ID') { + const formattedLabel = formatIndelTooltipLabelsCosmicStyle( + label, + category, + value, + valueLabel + ); + return formattedLabel !== '' ? formattedLabel : label; + } else { + return label; + } +} + +function formatIndelTooltipLabelsCosmicStyle( + label: string, + information: ColorMapProps[], + value: number, + valueLabel: string +): string { + if (information[0].category == 'Microhomology') { + return ( + value + + ' of small indels (' + + percentageToFraction(value) + + ') ' + + ' ' + + information[0].category + + '\n' + + ' with microhomology length ' + + label.split('_')[3] + ); + } else if (information[0].category == '>1bp insertion') { + return ( + value + + ' of small indels (' + + percentageToFraction(value) + + ') ' + + ' ' + + information[0].category + + '\n' + + ' with number of repeat units ' + + label.split('_')[3] + ); + } else if (information[0].category == '>1bp deletion') { + return ( + value + + ' of small indels (' + + percentageToFraction(value) + + ') ' + + ' ' + + information[0].category + + '\n' + + ' with number of repeat units ' + + label.split('_')[3] + ); + } else if (information[0].category == '1bp deletion') { + return ( + value + + ' of small indels (' + + percentageToFraction(value) + + ') ' + + ' ' + + information[0].category + + '(' + + information[0].subcategory + + ')' + + '\n' + + ' with homopolymer length' + + '\n' + + 'of ' + + label.split('_')[3] + ); + } else if (information[0].category == '1bp insertion') { + return ( + value + + ' of small indels (' + + percentageToFraction(value) + + ') ' + + ' ' + + information[0].category + + ' (' + + information[0].subcategory + + ')' + + '\n' + + ' with homopolymer length' + + '\n' + + 'of ' + + label.split('_')[3] + ); + } else { + return ''; + } +} + +export function formatMutationalSignatureLabel(label: string, version: string) { + if (version === 'SBS') { + return label.replace(/[\[\]]/g, '_').replace(/>/g, '-'); + } else if (version === 'DBS') { + return label.replace(/>/g, '-'); + } else if (version === 'ID') { + return label.replace(/:/g, '_'); + } else { + return label; + } +} + +function percentageToFraction(percentage: number): string { + // Function to find the greatest common divisor (GCD) + const findGCD = (a: number, b: number): number => { + while (b !== 0) { + const temp = b; + b = a % b; + a = temp; + } + return a; + }; + + const decimal: number = percentage / 100; + + // Convert the decimal to a fraction + let numerator: number = decimal * 1000000; + let denominator: number = 1000000; + + // Find the greatest common divisor + const gcd: number = findGCD(numerator, denominator); + + // Simplify the fraction + numerator /= gcd; + denominator /= gcd; + + const simplifiedFraction: string = `${numerator}/${denominator}`; + + return simplifiedFraction; +} diff --git a/src/pages/patientView/mutationalSignatures/MutationalSignaturesContainer.tsx b/src/pages/patientView/mutationalSignatures/MutationalSignaturesContainer.tsx index c34eb7e049f..99a2339e5f6 100644 --- a/src/pages/patientView/mutationalSignatures/MutationalSignaturesContainer.tsx +++ b/src/pages/patientView/mutationalSignatures/MutationalSignaturesContainer.tsx @@ -1,37 +1,37 @@ import * as React from 'react'; -import { Observer, observer } from 'mobx-react'; +import { observer } from 'mobx-react'; import { computed, action, makeObservable, observable } from 'mobx'; import { IMutationalSignature, IMutationalCounts, } from 'shared/model/MutationalSignature'; -import ClinicalInformationMutationalSignatureTable from '../clinicalInformation/ClinicalInformationMutationalSignatureTable'; +import ClinicalInformationMutationalSignatureTable, { + IMutationalSignatureRow, +} from '../clinicalInformation/ClinicalInformationMutationalSignatureTable'; import Select from 'react-select'; import autobind from 'autobind-decorator'; +import FeatureInstruction from 'shared/FeatureInstruction/FeatureInstruction'; -import { - ClinicalDataBySampleId, - MolecularProfile, - Sample, -} from 'cbioportal-ts-api-client'; +import { MolecularProfile } from 'cbioportal-ts-api-client'; import { getVersionOption, getVersionOptions, getSampleOptions, getSampleOption, - MutationalSignaturesVersion, } from 'shared/lib/GenericAssayUtils/MutationalSignaturesUtils'; import _ from 'lodash'; -import { ButtonGroup, Button } from 'react-bootstrap'; +import { ButtonGroup } from 'react-bootstrap'; import MutationalBarChart from 'pages/patientView/mutationalSignatures/MutationalSignatureBarChart'; -import { getPercentageOfMutationalCount } from './MutationalSignatureBarChartUtils'; import { - DefaultTooltip, - placeArrowBottomLeft, - DownloadControls, -} from 'cbioportal-frontend-commons'; + formatMutationalSignatureLabel, + getPercentageOfMutationalCount, + prepareMutationalSignatureDataForTable, +} from './MutationalSignatureBarChartUtils'; +import { DefaultTooltip, DownloadControls } from 'cbioportal-frontend-commons'; import classNames from 'classnames'; +import { MutationalSignatureTableDataStore } from 'pages/patientView/mutationalSignatures/MutationalSignaturesDataStore'; +import WindowStore from 'shared/components/window/WindowStore'; export interface IMutationalSignaturesContainerProps { data: { [version: string]: IMutationalSignature[] }; @@ -39,11 +39,15 @@ export interface IMutationalSignaturesContainerProps { version: string; sample: string; samples: string[]; + samplesNotProfiled: string[]; onVersionChange: (version: string) => void; onSampleChange: (sample: string) => void; dataCount: { [version: string]: IMutationalCounts[] }; } +const CONTENT_TO_SHOW_ABOVE_TABLE = + 'Click on a mutational signature to update reference plot'; + interface IAxisScaleSwitchProps { onChange: (selectedScale: AxisScale) => void; selectedScale: AxisScale; @@ -59,21 +63,35 @@ export default class MutationalSignaturesContainer extends React.Component< IMutationalSignaturesContainerProps, {} > { - @observable signatureProfile: string; + public mutationalSignatureTableStore: MutationalSignatureTableDataStore; + @observable signatureProfile: string = this.props.data[ + this.props.version + ][0].meta.name; + @observable signatureToPlot: string = this.props.data[this.props.version][0] + .meta.name; private plotSvg: SVGElement | null = null; @observable signatureURL: string; @observable signatureDescription: string; @observable isSignatureInformationToolTipVisible: boolean = false; + @observable updateReferencePlot: boolean = false; public static defaultProps: Partial = { selectedScale: AxisScale.COUNT, }; @observable - selectedScale: string = AxisScale.COUNT; + selectedScale: string = AxisScale.PERCENT; - mutationalProfileSelection = (childData: string, visibility: boolean) => { + mutationalProfileSelection = ( + childData: string, + visibility: boolean, + updateReference: boolean + ) => { this.signatureProfile = childData; + this.updateReferencePlot = updateReference; this.isSignatureInformationToolTipVisible = visibility; + this.signatureToPlot = updateReference + ? childData + : this.signatureToPlot; this.signatureURL = this.props.data[this.props.version].filter(obj => { return childData === obj.meta.name; @@ -94,6 +112,12 @@ export default class MutationalSignaturesContainer extends React.Component< constructor(props: IMutationalSignaturesContainerProps) { super(props); makeObservable(this); + + this.mutationalSignatureTableStore = new MutationalSignatureTableDataStore( + () => { + return this.mutationalSignatureDataForTable; + } + ); } @observable _selectedData: IMutationalCounts[] = this.props.dataCount[ @@ -158,18 +182,32 @@ export default class MutationalSignaturesContainer extends React.Component< ); } - @observable yAxisLabel: string = 'Mutational count (value)'; + + @observable currentVersion: string = this.props.version; + @observable yLabelString: string = this.updateYaxisLabel; + @observable yAxisLabel: string = this.yLabelString; @action.bound private handlePercentClick() { this.selectedScale = AxisScale.PERCENT; - this.yAxisLabel = 'Mutational count (%)'; + this.yAxisLabel = this.updateYaxisLabel; + } + + @computed get updateYaxisLabel() { + const unitAxis = this.selectedScale == '%' ? ' (%)' : ' (count)'; + const yLabel = + this.currentVersion == 'SBS' + ? 'Single Base Substitution' + : this.currentVersion == 'DBS' + ? 'Double Base Substitutions' + : 'Indels'; + return yLabel + unitAxis; } @action.bound private handleCountClick() { this.selectedScale = AxisScale.COUNT; - this.yAxisLabel = 'Mutational count (value)'; + this.yAxisLabel = this.updateYaxisLabel; } @computed get getDataForGraph(): IMutationalCounts[] { @@ -181,18 +219,23 @@ export default class MutationalSignaturesContainer extends React.Component< return this.mutationalSignatureCountDataGroupedByVersionForSample; } } - @observable _mutationalSignatureCountDataGroupedByVersionForSample: IMutationalCounts[]; - @computed get mutationalSignatureCountDataGroupedByVersionForSample() { - const sampleIdToFilter = this.props.samples.includes(this.props.sample) - ? this.props.sample - : undefined; + @computed + get mutationalSignatureCountDataGroupedByVersionForSample(): IMutationalCounts[] { return ( this._mutationalSignatureCountDataGroupedByVersionForSample || this.props.dataCount[this.props.version] - .map(item => item) - .filter(subItem => subItem.sampleId === sampleIdToFilter) + .map((obj, index) => { + obj[ + 'mutationalSignatureLabel' + ] = formatMutationalSignatureLabel( + obj.mutationalSignatureLabel, + this.props.version + ); + return obj; + }) + .filter(subItem => subItem.sampleId === this.sampleIdToFilter) ); } @@ -200,11 +243,16 @@ export default class MutationalSignaturesContainer extends React.Component< private onVersionChange(option: { label: string; value: string }): void { this.props.onVersionChange(option.value); this.signatureProfile = this.props.data[option.value][0].meta.name; + this.signatureToPlot = this.props.data[option.value][0].meta.name; + this.updateReferencePlot = false; this.isSignatureInformationToolTipVisible = false; + this.currentVersion = option.value; + this.yAxisLabel = this.updateYaxisLabel; } - @action.bound + @autobind private onSampleChange(sample: { label: string; value: string }): void { + this.getTotalMutationalCount; this.props.onSampleChange(sample.value); } @@ -217,16 +265,81 @@ export default class MutationalSignaturesContainer extends React.Component< return this.plotSvg; } + @autobind + onMutationalSignatureTableRowClick(d: IMutationalSignatureRow) { + this.signatureProfile = d.name; + this.signatureURL = d.url; + this.signatureProfile = d.name; + this.signatureToPlot = d.name; + this.updateReferencePlot = true; + this.mutationalSignatureTableStore.setSelectedMutSig(d); + if (this.mutationalSignatureTableStore.selectedMutSig.length > 0) { + this.mutationalSignatureTableStore.setclickedMutSig(d); + this.mutationalSignatureTableStore.toggleSelectedMutSig( + this.mutationalSignatureTableStore.selectedMutSig[0] + ); + } + } + + @autobind + onMutationalSignatureTableMouseOver(d: IMutationalSignatureRow) { + this.signatureProfile = d.name; + this.signatureURL = d.url; + this.signatureProfile = d.name; + } + + @computed get mutationalSignatureDataForTable() { + return prepareMutationalSignatureDataForTable( + this.props.data[this.props.version], + this.props.samples + ); + } + + @computed get sampleIdToFilter() { + return this.props.samples.includes(this.props.sample) + ? this.props.sample + : undefined; + } + + @computed get getTotalMutationalCount() { + const countPerVersion = this.props.dataCount[this.props.version] + .filter(subItem => subItem.sampleId === this.sampleIdToFilter) + .map(item => { + return item.value; + }); + const mutTotalCount = countPerVersion.reduce((a, b) => a + b, 0); + return [this.props.version, mutTotalCount]; + } + public render() { return (
-
+ {this.props.samplesNotProfiled.length > 0 && ( +
+ + {this.props.samplesNotProfiled.length > 1 + ? this.props.samplesNotProfiled.join(',') + : this.props.samplesNotProfiled}{' '} + {this.props.samplesNotProfiled.length > 1 + ? ' are' + : ' is'}{' '} + not profiled for mutational signatures + +
+ )} +
- Version: + Variant Class: + + + Mutational signature description + (COSMIC):{' '} + {' '} +
+ SBS: Single Base Substitution{' '} +
+ DBS: Double Base Substitution{' '} +
+ ID: Small Insertions and + Deletions
+ + } + destroyTooltipOnHide={true} + > + +