diff --git a/packages/cbioportal-clinical-timeline/src/CustomTrack.tsx b/packages/cbioportal-clinical-timeline/src/CustomTrack.tsx index b0f00eb1b29..6bcd4fe068b 100644 --- a/packages/cbioportal-clinical-timeline/src/CustomTrack.tsx +++ b/packages/cbioportal-clinical-timeline/src/CustomTrack.tsx @@ -5,10 +5,14 @@ import classNames from 'classnames'; export type CustomTrackSpecification = { renderHeader: (store: TimelineStore) => any; // any = react renderable, string or element or null or etc. - renderTrack: (store: TimelineStore) => React.ReactElement<SVGGElement>; + renderTrack: ( + store: TimelineStore, + headerTargetEl: any + ) => React.ReactElement<SVGGElement>; height: (store: TimelineStore) => number; labelForExport: string; disableHover?: boolean; + uid: string; }; export interface ICustomTrackProps { @@ -44,7 +48,12 @@ const CustomTrack: React.FunctionComponent<ICustomTrackProps> = function({ height={specification.height(store)} width={width} /> - {specification.renderTrack(store)} + {specification.renderTrack( + store, + document.getElementsByClassName( + `tl-track-uid-${specification.uid}` + )[0] + )} <line x1={0} x2={width} diff --git a/packages/cbioportal-clinical-timeline/src/CustomTrackHeader.tsx b/packages/cbioportal-clinical-timeline/src/CustomTrackHeader.tsx index 53756eabfb3..3010ab1961a 100644 --- a/packages/cbioportal-clinical-timeline/src/CustomTrackHeader.tsx +++ b/packages/cbioportal-clinical-timeline/src/CustomTrackHeader.tsx @@ -18,14 +18,18 @@ const CustomTrackHeader: React.FunctionComponent<ICustomTrackHeaderProps> = func }: ICustomTrackHeaderProps) { return ( <div - className={classNames('tl-custom-track-header', { - 'tl-hover-disabled': disableHover, - })} + className={classNames( + 'tl-custom-track-header', + `tl-track-uid-${specification.uid}`, + { + 'tl-hover-disabled': disableHover, + } + )} style={{ paddingLeft: 5, height: specification.height(store) }} onMouseEnter={handleTrackHover} onMouseLeave={handleTrackHover} > - {specification.renderHeader(store)} + {specification.renderHeader && specification.renderHeader(store)} </div> ); }; diff --git a/src/pages/patientView/timeline2/VAFChart.tsx b/src/pages/patientView/timeline2/VAFChart.tsx index 8bc9505a696..dc6776facc2 100644 --- a/src/pages/patientView/timeline2/VAFChart.tsx +++ b/src/pages/patientView/timeline2/VAFChart.tsx @@ -49,6 +49,7 @@ interface IVAFChartProps { mutationProfileId: string; coverageInformation: CoverageInformation; sampleManager: SampleManager; + headerEl?: HTMLDivElement; } const HIGHLIGHT_LINE_STROKE_WIDTH = 6; @@ -632,34 +633,36 @@ export default class VAFChart extends React.Component<IVAFChartProps, {}> { renderTrack: () => this.sampeIconsGroupByTrack(sampleIds), height: () => this.groupIndexToTrackHeight[index], labelForExport: this.clinicalValuesForGrouping[index], + uid: `groupbytracks-${index}`, }); }); this.props.wrapperStore.groupByTracks = tracks; } - yAxisHeaderReaction = autorun(() => { - this.renderHeader(this.ticks); - }); + // yAxisHeaderReaction = autorun(() => { + // this.renderHeader(this.ticks); + // }); groupByTracksReaction = autorun(() => { this.setGroupByTracks(this.sampleGroups); }); destroy() { - this.yAxisHeaderReaction(); + // this.yAxisHeaderReaction(); this.groupByTracksReaction(); } @action renderHeader(ticks: { label: string; value: number; offset: number }[]) { - this.props.wrapperStore.vafPlotHeader = (store: TimelineStore) => ( - <div className={'positionAbsolute'} style={{ right: -6 }}> - <VAFChartHeader - ticks={ticks} - legendHeight={this.props.wrapperStore.vafChartHeight} - /> - </div> - ); + return null; + // this.props.wrapperStore.vafPlotHeader = (store: TimelineStore) => ( + // <div className={'positionAbsolute'} style={{ right: -6 }}> + // <VAFChartHeader + // ticks={ticks} + // legendHeight={this.props.wrapperStore.vafChartHeight} + // /> + // </div> + // ); } @computed get groupColor() { @@ -672,44 +675,72 @@ export default class VAFChart extends React.Component<IVAFChartProps, {}> { render() { return ( - <svg - width={this.props.store.pixelWidth} - height={this.recalculateTotalHeight()} - > - {this.renderData.lineData.map( - (data: IPoint[], index: number) => { - return data.map((d: IPoint, i: number) => { - let x1 = this.xPosition[d.sampleId], - x2; - let y1 = this.yPosition[d.y], - y2; - - const nextPoint: IPoint = data[i + 1]; - if (nextPoint) { - x2 = this.xPosition[nextPoint.sampleId]; - y2 = this.yPosition[nextPoint.y]; - } - - let tooltipDatum: { - mutationStatus: MutationStatus; - sampleId: string; - vaf: number; - } = { - mutationStatus: d.mutationStatus, - sampleId: d.sampleId, - vaf: d.y, - }; - - const color = this.groupColor(d.sampleId); - - return ( - <g> - {x2 && y2 && ( - <VAFPointConnector - x1={x1} - y1={y1} - x2={x2} - y2={y2} + <> + {this.props.headerEl && ( + <Portal node={this.props.headerEl}> + <div + className={'positionAbsolute'} + style={{ right: -6 }} + > + <VAFChartHeader + ticks={this.ticks} + legendHeight={ + this.props.wrapperStore.vafChartHeight + } + /> + </div> + </Portal> + )} + <svg + width={this.props.store.pixelWidth} + height={this.recalculateTotalHeight()} + > + {this.renderData.lineData.map( + (data: IPoint[], index: number) => { + return data.map((d: IPoint, i: number) => { + let x1 = this.xPosition[d.sampleId], + x2; + let y1 = this.yPosition[d.y], + y2; + + const nextPoint: IPoint = data[i + 1]; + if (nextPoint) { + x2 = this.xPosition[nextPoint.sampleId]; + y2 = this.yPosition[nextPoint.y]; + } + + let tooltipDatum: { + mutationStatus: MutationStatus; + sampleId: string; + vaf: number; + } = { + mutationStatus: d.mutationStatus, + sampleId: d.sampleId, + vaf: d.y, + }; + + const color = this.groupColor(d.sampleId); + + return ( + <g> + {x2 && y2 && ( + <VAFPointConnector + x1={x1} + y1={y1} + x2={x2} + y2={y2} + color={color} + tooltipDatum={tooltipDatum} + mutation={d.mutation} + dataStore={this.props.dataStore} + wrapperStore={ + this.props.wrapperStore + } + /> + )} + <VAFPoint + x={x1} + y={y1} color={color} tooltipDatum={tooltipDatum} mutation={d.mutation} @@ -718,26 +749,17 @@ export default class VAFChart extends React.Component<IVAFChartProps, {}> { this.props.wrapperStore } /> - )} - <VAFPoint - x={x1} - y={y1} - color={color} - tooltipDatum={tooltipDatum} - mutation={d.mutation} - dataStore={this.props.dataStore} - wrapperStore={this.props.wrapperStore} - /> - </g> - ); - }); - } - )} + </g> + ); + }); + } + )} - {!this.groupingByIsSelected && this.sampleIcons()} - <Observer>{this.getHighlights}</Observer> - <Observer>{this.getTooltipComponent}</Observer> - </svg> + {!this.groupingByIsSelected && this.sampleIcons()} + <Observer>{this.getHighlights}</Observer> + <Observer>{this.getTooltipComponent}</Observer> + </svg> + </> ); } diff --git a/src/pages/patientView/timeline2/VAFChartWrapper.tsx b/src/pages/patientView/timeline2/VAFChartWrapper.tsx index f97b482a003..51fe9bc9665 100644 --- a/src/pages/patientView/timeline2/VAFChartWrapper.tsx +++ b/src/pages/patientView/timeline2/VAFChartWrapper.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { RefObject, useEffect, useState } from 'react'; import { observer } from 'mobx-react-lite'; import { CoverageInformation } from '../../resultsView/ResultsViewPageStoreUtils'; import { ClinicalEvent, Sample } from 'cbioportal-ts-api-client'; @@ -24,6 +24,7 @@ import { } from 'pages/patientView/timeline2/helpers'; import { CustomTrackSpecification } from 'cbioportal-clinical-timeline/dist/CustomTrack'; import { downloadZippedTracks } from 'pages/patientView/timeline2/timelineDataUtils'; +import { Portal } from 'react-overlays/lib'; export interface ISampleMetaDeta { color: { [sampleId: string]: string }; @@ -102,24 +103,30 @@ const VAFChartWrapper: React.FunctionComponent<IVAFChartWrapperProps> = observer const groupByTracks = wrapperStore.groupByTracks; const vafPlotTrack = { - renderHeader: wrapperStore.vafPlotHeader, - renderTrack: (store: TimelineStore) => ( - <VAFChart - dataStore={dataStore} - store={store} - wrapperStore={wrapperStore} - sampleMetaData={caseMetaData} - samples={samples} - mutationProfileId={mutationProfileId} - coverageInformation={coverageInformation} - sampleManager={sampleManager} - /> - ), + //renderHeader: wrapperStore.vafPlotHeader, + renderTrack: (store: TimelineStore, headerEl?: HTMLDivElement) => { + return ( + <> + <VAFChart + dataStore={dataStore} + store={store} + wrapperStore={wrapperStore} + sampleMetaData={caseMetaData} + samples={samples} + mutationProfileId={mutationProfileId} + coverageInformation={coverageInformation} + sampleManager={sampleManager} + headerEl={headerEl} + /> + </> + ); + }, disableHover: true, height: (store: TimelineStore) => { return wrapperStore.vafChartHeight; }, labelForExport: 'VAF', + uid: 'vafplot', } as CustomTrackSpecification; let customTracks = [vafPlotTrack].concat(wrapperStore.groupByTracks);