import * as React from 'react'; import { observer } from 'mobx-react'; import { action, computed, observable, toJS, reaction, IReactionDisposer, makeObservable, } from 'mobx'; import autobind from 'autobind-decorator'; import _ from 'lodash'; import LabeledCheckbox from 'shared/components/labeledCheckbox/LabeledCheckbox'; import { ClinicalDataCountSummary } from 'pages/studyView/StudyViewUtils'; import FixedHeaderTable from './FixedHeaderTable'; import styles from './tables.module.scss'; import { correctColumnWidth, correctMargin, getClinicalAttributeOverlay, getFixedHeaderNumberCellMargin, getFixedHeaderTableMaxLengthStringPixel, getFrequencyStr, } from '../StudyViewUtils'; import { SortDirection } from '../../../shared/components/lazyMobXTable/LazyMobXTable'; import { EllipsisTextTooltip } from 'cbioportal-frontend-commons'; import { DEFAULT_SORTING_COLUMN } from '../StudyViewConfig'; import ComparisonVsIcon from 'shared/components/ComparisonVsIcon'; export interface IClinicalTableProps { data: ClinicalDataCountSummary[]; filters: string[]; highlightedRow?: (value: string | undefined) => void; onUserSelection: (values: string[]) => void; openComparisonPage?: () => void; label?: string; labelDescription?: string; patientAttribute: boolean; width?: number; height?: number; showAddRemoveAllButtons?: boolean; } class ClinicalTableComponent extends FixedHeaderTable< ClinicalDataCountSummary > {} export enum ColumnKey { CATEGORY = 'Category', NUMBER = '#', FREQ = 'Freq', } @observer export default class ClinicalTable extends React.Component< IClinicalTableProps, {} > { constructor(props: IClinicalTableProps) { super(props); makeObservable(this); } @observable private sortBy: string = DEFAULT_SORTING_COLUMN; @observable private sortDirection: SortDirection; static readonly defaultProps = { width: 300, showAddRemoveAllButtons: false, }; @computed get columnsWidth() { const numberWidth = 80; const freqWidth = 60; const categoryWidth = correctColumnWidth( this.props.width! - (numberWidth + freqWidth) ); return { [ColumnKey.CATEGORY]: categoryWidth, [ColumnKey.NUMBER]: numberWidth, [ColumnKey.FREQ]: freqWidth, }; } @computed get cellMargin() { return { [ColumnKey.CATEGORY]: 0, [ColumnKey.NUMBER]: this.props.data.length === 0 ? 0 : correctMargin( (this.columnsWidth[ColumnKey.NUMBER] - 10 - (getFixedHeaderTableMaxLengthStringPixel( _.max( this.props.data.map(item => item.count) )!.toLocaleString() ) + 20)) / 2 ), [ColumnKey.FREQ]: this.props.data.length === 0 ? 0 : correctMargin( getFixedHeaderNumberCellMargin( this.columnsWidth[ColumnKey.FREQ], _.sortBy( this.props.data, item => item.percentage )[this.props.data.length - 1].freq ) ), }; } @computed get firstColumnName() { return this.props.label ? this.props.label : ColumnKey.CATEGORY; } @computed private get columns() { return [ { name: this.firstColumnName, render: (data: ClinicalDataCountSummary) => { return (
{ this.tooltipLabelMouseEnter(data.value); }} onMouseLeave={this.tooltipLabelMouseLeave} >
); }, headerRender: () => { const style: any = {}; let text = this.firstColumnName; if (!this.props.label) { style.opacity = 0; text = '.'; } return ( ); }, tooltip: getClinicalAttributeOverlay( this.firstColumnName, this.props.labelDescription ? this.props.labelDescription : '' ), filter: ( d: ClinicalDataCountSummary, f: string, filterStringUpper: string ) => d.value.toUpperCase().includes(filterStringUpper), sortBy: (d: ClinicalDataCountSummary) => d.value, defaultSortDirection: 'asc' as 'asc', width: this.columnsWidth[ColumnKey.CATEGORY], }, { name: ColumnKey.NUMBER, headerRender: () => { return (
#
); }, render: (data: ClinicalDataCountSummary) => ( this.onUserSelection(data.value)} labelProps={{ style: { display: 'flex', justifyContent: 'space-between', marginLeft: this.cellMargin[ColumnKey.NUMBER], marginRight: this.cellMargin[ColumnKey.NUMBER], }, }} inputProps={{ className: styles.autoMarginCheckbox, }} > {data.count.toLocaleString()} ), tooltip: ( Number of{' '} {this.props.patientAttribute ? 'patients' : 'samples'} ), filter: (d: ClinicalDataCountSummary, f: string) => d.count.toString().includes(f), sortBy: (d: ClinicalDataCountSummary) => d.count, defaultSortDirection: 'desc' as 'desc', width: this.columnsWidth[ColumnKey.NUMBER], }, { name: ColumnKey.FREQ, headerRender: () => { return (
Freq
); }, render: (data: ClinicalDataCountSummary) => ( {data.freq} ), tooltip: ( Percentage of{' '} {this.props.patientAttribute ? 'patients' : 'samples'} ), filter: (d: ClinicalDataCountSummary, f: string) => { return d.freq.includes(f); }, sortBy: (d: ClinicalDataCountSummary) => d.percentage, //sort freq column using count defaultSortDirection: 'desc' as 'desc', width: this.columnsWidth[ColumnKey.FREQ], }, ]; } @autobind private onUserSelection(filter: string) { let filters = toJS(this.props.filters); if (_.includes(filters, filter)) { filters = _.filter(filters, obj => obj !== filter); } else { filters.push(filter); } this.props.onUserSelection(filters); } @autobind private tooltipLabelMouseEnter(value: string): void { if (this.props.highlightedRow) { this.props.highlightedRow(value); } } @autobind private tooltipLabelMouseLeave(): void { if (this.props.highlightedRow) { this.props.highlightedRow(undefined); } } @autobind addAll(selectedRows: ClinicalDataCountSummary[]) { this.props.onUserSelection(selectedRows.map(row => row.value)); } @autobind removeAll(deselectedRows: ClinicalDataCountSummary[]) { const deselectRows = deselectedRows.map(row => row.value); this.props.onUserSelection( this.props.filters.filter(filter => !deselectRows.includes(filter)) ); } @action.bound afterSorting(sortBy: string, sortDirection: SortDirection) { this.sortBy = sortBy; this.sortDirection = sortDirection; } @computed get selectedClinicalValues() { const filtersMap = _.keyBy(this.props.filters); return this.props.data.filter(d => d.value in filtersMap); } @computed get extraButtons() { if (this.props.openComparisonPage) { return [ { content: (
Compare
), onClick: this.props.openComparisonPage, isDisabled: () => this.selectedClinicalValues.length === 1, // no selection means compare all, so only problem would be if exactly 1 is selected }, ]; } else { return []; } } render() { return ( ); } }