import * as React from 'react'; import { observer } from 'mobx-react'; import { makeObservable } from 'mobx'; import Oncoprint, { IBaseHeatmapTrackDatum, IBaseHeatmapTrackSpec, IHeatmapTrackSpec, } from 'shared/components/oncoprint/Oncoprint'; import autobind from 'autobind-decorator'; import { InitParams, OncoprintJS } from 'oncoprintjs'; import _ from 'lodash'; import { IStringAxisData } from 'pages/resultsView/plots/PlotsTabUtils'; export type IOncoprintHeatmapProps = { horzData?: IStringAxisData['data']; vertData?: IStringAxisData['data']; axisLabelX: string; barWidth: number; groupToColor?: { [group: string]: string }; broadcastOncoprintJsRef: (oncoprint: OncoprintJS) => void; }; const MIN_WIDTH = 400; const INIT_PARAMS: InitParams = { init_cell_width: 20, init_cell_padding: 1, cell_padding_off_cell_width_threshold: 10, }; @observer export default class MultipleCategoryHeatmap extends React.Component< IOncoprintHeatmapProps, {} > { private oncoprintJs: OncoprintJS | null = null; private oncoprint: Oncoprint | null = null; constructor(props: any) { super(props); } @autobind private oncoprintRef(oncoprint: Oncoprint | null) { this.oncoprint = oncoprint; } @autobind private oncoprintJsRef(oncoprint: OncoprintJS) { this.oncoprintJs = oncoprint; this.props.broadcastOncoprintJsRef(oncoprint); } render() { const columnLabels = toColumnLabels(this.props); let heatmapTracks = toHeatmapTracks(this.props, columnLabels); const width = calcWidth(this.props.barWidth, columnLabels, MIN_WIDTH); heatmapTracks = _.sortBy(heatmapTracks, ['key']); heatmapTracks.forEach(t => (t.data = _.sortBy(t.data, ['uid']))); return (
 
); } } function calcWidth(barWidth: number, columnLabels: any, minWidth: number) { let width = barWidth * Object.keys(columnLabels).length * 1.2; if (width < MIN_WIDTH) { width = MIN_WIDTH; } return width; } function toColumnLabels(props: IOncoprintHeatmapProps): any { if (!props.vertData) { return {}; } return props.vertData.reduce((result: any, element) => { result[element.value as string] = { text: element.value, }; return result; }, {}); } function toHeatmapTracks( props: IOncoprintHeatmapProps, categories: string[] ): IBaseHeatmapTrackSpec[] { if (!props.horzData || !props.vertData) { return []; } let groups = props.horzData.reduce((arr: IBaseHeatmapTrackSpec[], next) => { const trackKey = Array.isArray(next.value) ? next.value[0] : next.value; let track = arr.find(r => r.key === trackKey); const column = props.vertData!.find( vd => vd.uniqueSampleKey === next.uniqueSampleKey ); if (!column) { // When categories are filtered, i.e. when NA is not shown: return arr; } const columnKey = Array.isArray(column.value) ? column.value[0] : column.value; if (!track) { track = createTrack(trackKey, categories, props); arr.push(track); } let cell = track.data.find(gd => gd.uid === columnKey); cell!.profile_data!++; return arr; }, []); const groupsPercentages: number[][] = []; // Create clone to prevent changing data // that is used in other places as well: groups = _.cloneDeep(groups); groups.forEach(group => { const groupSampleCount = group.data.reduce( (partialSum: number, d) => partialSum + (d.profile_data || 0), 0 ); const groupPercentages: number[] = []; groupsPercentages.push(groupPercentages); group.data.forEach(d => { const sampleCount = d.profile_data || 0; const percentage = (sampleCount / groupSampleCount) * 100; groupPercentages.push(percentage); // Tooltip label: d.sample = `${ d.sample }; ${sampleCount} samples (${percentage.toFixed(1)}%)`; }); }); // Calculate 'heat' (value between 0 and 1) of heatmap cells: const percentageMax = Math.max(..._.flatten(groupsPercentages)); groups.forEach((group, gi) => { group.data.forEach((d, di) => { d.profile_data = groupsPercentages[gi][di] / percentageMax; }); }); return groups; } function createTrack( key: string, binKeys: string[], props: IOncoprintHeatmapProps ) { const groupBins = Object.keys(binKeys).map(k => { return { uid: k, profile_data: 0, study_id: '', } as IBaseHeatmapTrackDatum; }); const labelLetters = key.match(/[a-zA-Z0-9]{1}/); const label = `${labelLetters ? labelLetters[0] : '-'}`; const labelCircleColor = props.groupToColor && props.groupToColor[key] ? props.groupToColor[key] : 'black'; return { key, label, labelCircleColor, description: key, data: _.cloneDeep(groupBins).map(b => { b.sample = `group: ${key}; category: ${b.uid}`; return b; }) as IBaseHeatmapTrackDatum[], molecularProfileId: '', trackGroupIndex: 2, naLegendLabel: 'No data', labelColor: 'white', labelFontWeight: 'normal', labelLeftPadding: 21, hasColumnSpacing: true, sortDirectionChangeable: false, initSortDirection: -1, movable: false, legendLabel: props.axisLabelX, // Color scheme: molecularAlterationType: 'MUTATION_EXTENDED', } as IHeatmapTrackSpec; }