From 8f83fcce4c1cfbc4f77fe69848dd1cd0ab632370 Mon Sep 17 00:00:00 2001 From: mukayevolzhas Date: Thu, 25 Jul 2024 01:04:27 +0300 Subject: [PATCH] feat: add category bar chart --- src/pages/studyView/StudyViewConfig.ts | 27 ++ src/pages/studyView/StudyViewPageStore.ts | 85 ++++- src/pages/studyView/StudyViewUtils.tsx | 18 + .../studyView/chartHeader/ChartHeader.tsx | 140 ++++--- src/pages/studyView/charts/ChartContainer.tsx | 44 ++- .../categoryBarChart/CategoryBarChart.tsx | 353 ++++++++++++++++++ .../CategoryBarChartAxisLabel.tsx | 70 ++++ .../CategoryBarChartToolTip.tsx | 95 +++++ .../categoryBarChart/CustomBinsModal.tsx | 313 ++++++++++++++++ src/pages/studyView/tabs/SummaryTab.tsx | 29 ++ 10 files changed, 1105 insertions(+), 69 deletions(-) create mode 100644 src/pages/studyView/charts/categoryBarChart/CategoryBarChart.tsx create mode 100644 src/pages/studyView/charts/categoryBarChart/CategoryBarChartAxisLabel.tsx create mode 100644 src/pages/studyView/charts/categoryBarChart/CategoryBarChartToolTip.tsx create mode 100644 src/pages/studyView/charts/categoryBarChart/CustomBinsModal.tsx diff --git a/src/pages/studyView/StudyViewConfig.ts b/src/pages/studyView/StudyViewConfig.ts index 166ef974a1f..5bfb5976e1b 100644 --- a/src/pages/studyView/StudyViewConfig.ts +++ b/src/pages/studyView/StudyViewConfig.ts @@ -23,6 +23,7 @@ export type StudyViewColorTheme = { export type StudyViewThreshold = { pieToTable: number; + pieToBar: number; piePadding: number; barRatio: number; rowsInTableForOneGrid: number; @@ -61,9 +62,14 @@ export type StudyViewFrontEndConfig = { export type StudyViewConfig = StudyView & StudyViewFrontEndConfig; +export type ChangeChartOptionsMap = { + [chartType in ChartTypeEnum]?: ChartType[]; +}; + export enum ChartTypeEnum { PIE_CHART = 'PIE_CHART', BAR_CHART = 'BAR_CHART', + BAR_CATEGORICAL_CHART = 'BAR_CATEGORICAL_CHART', SURVIVAL = 'SURVIVAL', TABLE = 'TABLE', SCATTER = 'SCATTER', @@ -88,6 +94,7 @@ export enum ChartTypeEnum { export enum ChartTypeNameEnum { PIE_CHART = 'pie chart', BAR_CHART = 'bar chart', + BAR_CATEGORICAL_CHART = 'categorical bar chart', SURVIVAL = 'survival plot', TABLE = 'table', SCATTER = 'density plot', @@ -160,6 +167,7 @@ const studyViewFrontEnd = { }, thresholds: { pieToTable: 20, + pieToBar: 5, piePadding: 20, barRatio: 0.8, rowsInTableForOneGrid: 4, @@ -186,6 +194,10 @@ const studyViewFrontEnd = { w: 2, h: 1, }, + [ChartTypeEnum.BAR_CATEGORICAL_CHART]: { + w: 2, + h: 1, + }, [ChartTypeEnum.SCATTER]: { w: 2, h: 2, @@ -312,3 +324,18 @@ export const STUDY_VIEW_CONFIG: StudyViewConfig = _.assign( studyViewFrontEnd, (getServerConfig() || {}).study_view ); + +export const chartChangeOptionsMap: ChangeChartOptionsMap = { + [ChartTypeEnum.PIE_CHART]: [ + ChartTypeEnum.BAR_CATEGORICAL_CHART, + ChartTypeEnum.TABLE, + ], + [ChartTypeEnum.TABLE]: [ + ChartTypeEnum.PIE_CHART, + ChartTypeEnum.BAR_CATEGORICAL_CHART, + ], + [ChartTypeEnum.BAR_CATEGORICAL_CHART]: [ + ChartTypeEnum.PIE_CHART, + ChartTypeEnum.TABLE, + ], +}; diff --git a/src/pages/studyView/StudyViewPageStore.ts b/src/pages/studyView/StudyViewPageStore.ts index 98d9da7d788..48ab882d2ba 100644 --- a/src/pages/studyView/StudyViewPageStore.ts +++ b/src/pages/studyView/StudyViewPageStore.ts @@ -2155,6 +2155,11 @@ export class StudyViewPageStore ChartDimension >(); + @observable public availableChartTypes = observable.map< + ChartUniqueKey, + ChartType[] + >(); + @observable public chartsType = observable.map(); private newlyAddedCharts = observable.array(); @@ -4532,7 +4537,7 @@ export class StudyViewPageStore data.forEach(item => { const uniqueKey = item.attributeId; if (this.isNewlyAdded(uniqueKey)) { - this.showAsPieChart(uniqueKey, item.counts.length); + this.showAsPieChart(uniqueKey, item.counts); this.newlyAddedCharts.remove(uniqueKey); } }); @@ -4559,7 +4564,7 @@ export class StudyViewPageStore data.forEach(item => { const uniqueKey = item.attributeId; if (this.isNewlyAdded(uniqueKey)) { - this.showAsPieChart(uniqueKey, item.counts.length); + this.showAsPieChart(uniqueKey, item.counts); this.newlyAddedCharts.remove(uniqueKey); } }); @@ -4588,7 +4593,7 @@ export class StudyViewPageStore data.forEach(item => { const uniqueKey = item.attributeId; this.unfilteredClinicalDataCountCache[uniqueKey] = item; - this.showAsPieChart(uniqueKey, item.counts.length); + this.showAsPieChart(uniqueKey, item.counts); this.newlyAddedCharts.remove(uniqueKey); }); }, @@ -5843,7 +5848,10 @@ export class StudyViewPageStore onError: () => {}, onResult: clinicalAttributes => { clinicalAttributes.forEach((obj: ClinicalAttribute) => { - if (obj.datatype === DataType.NUMBER) { + if ( + obj.datatype === DataType.NUMBER || + obj.datatype === DataType.STRING + ) { const uniqueKey = getUniqueKey(obj); let filter = getDefaultClinicalDataBinFilter(obj); @@ -7600,7 +7608,7 @@ export class StudyViewPageStore if (this.queriedPhysicalStudyIds.result.length > 1) { this.showAsPieChart( SpecialChartsUniqueKeyEnum.CANCER_STUDIES, - this.queriedPhysicalStudyIds.result.length + this.queriedPhysicalStudyIds.result ); } } @@ -7643,7 +7651,7 @@ export class StudyViewPageStore this.getTableDimensionByNumberOfRecords(data.result!.length) ); } - this.chartsType.set(attr.uniqueKey, ChartTypeEnum.TABLE); + this.chartsType.set(attr.uniqueKey, newChartType); } else { this.chartsDimension.set( attr.uniqueKey, @@ -7792,7 +7800,7 @@ export class StudyViewPageStore _.each( this.initialVisibleAttributesClinicalDataCountData.result, item => { - this.showAsPieChart(item.attributeId, item.counts.length); + this.showAsPieChart(item.attributeId, item.counts); } ); } @@ -9879,31 +9887,84 @@ export class StudyViewPageStore }); @action - showAsPieChart(uniqueKey: string, dataSize: number): void { + showAsPieChart( + uniqueKey: string, + data: ClinicalDataCount[] | string[] + ): void { + const { totalCount, naCount } = (data as ( + | ClinicalDataCount + | string + )[]).reduce( + (acc: { totalCount: number; naCount: number }, item) => { + if ( + typeof item === 'object' && + 'value' in item && + 'count' in item + ) { + acc.totalCount += item.count; + if (item.value === 'NA') { + acc.naCount += item.count; + } + } + return acc; + }, + { totalCount: 0, naCount: 0 } + ); + + const naProportion = totalCount > 0 ? naCount / totalCount : 0; + if ( shouldShowChart( this.initialFilters, - dataSize, + data.length, this.samples.result.length ) ) { this.changeChartVisibility(uniqueKey, true); if ( - dataSize > STUDY_VIEW_CONFIG.thresholds.pieToTable || - _.includes(STUDY_VIEW_CONFIG.tableAttrs, uniqueKey) + data.length > STUDY_VIEW_CONFIG.thresholds.pieToTable || + STUDY_VIEW_CONFIG.tableAttrs.includes(uniqueKey) ) { this.chartsType.set(uniqueKey, ChartTypeEnum.TABLE); this.chartsDimension.set( uniqueKey, - this.getTableDimensionByNumberOfRecords(dataSize) + this.getTableDimensionByNumberOfRecords(data.length) + ); + this.availableChartTypes.set(uniqueKey, [ + ChartTypeEnum.PIE_CHART, + ChartTypeEnum.TABLE, + ]); + } else if ( + data.length > STUDY_VIEW_CONFIG.thresholds.pieToBar || + naProportion > 0.5 + ) { + this.chartsType.set( + uniqueKey, + ChartTypeEnum.BAR_CATEGORICAL_CHART + ); + this.chartsDimension.set( + uniqueKey, + STUDY_VIEW_CONFIG.layout.dimensions[ + ChartTypeEnum.BAR_CATEGORICAL_CHART + ] ); + this.availableChartTypes.set(uniqueKey, [ + ChartTypeEnum.PIE_CHART, + ChartTypeEnum.BAR_CATEGORICAL_CHART, + ChartTypeEnum.TABLE, + ]); } else { this.chartsType.set(uniqueKey, ChartTypeEnum.PIE_CHART); this.chartsDimension.set( uniqueKey, STUDY_VIEW_CONFIG.layout.dimensions[ChartTypeEnum.PIE_CHART] ); + this.availableChartTypes.set(uniqueKey, [ + ChartTypeEnum.PIE_CHART, + ChartTypeEnum.BAR_CATEGORICAL_CHART, + ChartTypeEnum.TABLE, + ]); } } } diff --git a/src/pages/studyView/StudyViewUtils.tsx b/src/pages/studyView/StudyViewUtils.tsx index e2e8b78bc78..86f7ba89e27 100644 --- a/src/pages/studyView/StudyViewUtils.tsx +++ b/src/pages/studyView/StudyViewUtils.tsx @@ -226,6 +226,12 @@ export type DataBin = { start: number; }; +export type CategoryDataBin = { + id: string; + count: number; + specialValue: string; +}; + export type MutationCategorization = 'MUTATED' | 'MUTATION_TYPE'; export const SPECIAL_CHARTS: ChartMetaWithDimensionAndChartType[] = [ @@ -1246,6 +1252,18 @@ export function filterIntervalBins(numericalBins: DataBin[]) { ); } +export function clinicalDataToDataBin( + data: ClinicalDataCountSummary[] +): DataBin[] { + return data.map(item => ({ + id: item.value, + count: item.count, + specialValue: `${item.value}`, + start: -1, + end: -1, + })); +} + export function calcIntervalBinValues(intervalBins: DataBin[]) { const values = intervalBins.map(dataBin => dataBin.start); diff --git a/src/pages/studyView/chartHeader/ChartHeader.tsx b/src/pages/studyView/chartHeader/ChartHeader.tsx index 83a273aa57d..a921ce7a343 100644 --- a/src/pages/studyView/chartHeader/ChartHeader.tsx +++ b/src/pages/studyView/chartHeader/ChartHeader.tsx @@ -8,7 +8,7 @@ import { import classnames from 'classnames'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { ChartTypeEnum } from '../StudyViewConfig'; +import { ChartTypeEnum, ChartTypeNameEnum } from '../StudyViewConfig'; import { ChartMeta, getClinicalAttributeOverlay } from '../StudyViewUtils'; import { DataType, @@ -82,6 +82,7 @@ export interface ChartControls { showSwapAxes?: boolean; showSurvivalPlotLeftTruncationToggle?: boolean; survivalPlotLeftTruncationChecked?: boolean; + showChartChangeOptions?: boolean; } @observer @@ -90,6 +91,7 @@ export class ChartHeader extends React.Component { @observable downloadSubmenuOpen = false; @observable comparisonSubmenuOpen = false; @observable showCustomBinModal: boolean = false; + @observable showChartChangeOptions: boolean = false; private closeMenuTimeout: number | undefined = undefined; constructor(props: IChartHeaderProps) { @@ -414,6 +416,85 @@ export class ChartHeader extends React.Component { ); } + if ( + this.props.chartControls && + !!this.props.chartControls.showChartChangeOptions + ) { + const submenuWidth = 120; + const availableCharts = + this.props.store.availableChartTypes.get( + this.props.chartMeta.uniqueKey + ) || []; + items.push( +
  • +
    + (this.showChartChangeOptions = true) + } + onMouseLeave={() => + (this.showChartChangeOptions = false) + } + > +
    +