From 036ff5d19f19c2cb4169dec504a8fa7fb6da9b35 Mon Sep 17 00:00:00 2001 From: Ruslan Forostianov Date: Wed, 21 Oct 2020 16:32:03 +0200 Subject: [PATCH 1/2] Adjust fractional part of VAF cart ticks to be able to distinquish tick labels --- .../timeline2/VAFChartUtils.spec.ts | 31 +++++++++++++++++++ .../patientView/timeline2/VAFChartUtils.ts | 28 +++++++++++++++++ .../patientView/timeline2/VAFChartWrapper.tsx | 13 +++++--- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/pages/patientView/timeline2/VAFChartUtils.spec.ts b/src/pages/patientView/timeline2/VAFChartUtils.spec.ts index dd03279ffb5..732a0f9d6c2 100644 --- a/src/pages/patientView/timeline2/VAFChartUtils.spec.ts +++ b/src/pages/patientView/timeline2/VAFChartUtils.spec.ts @@ -11,6 +11,7 @@ import { IPoint, numLeadingDecimalZeros, round10, + minimalDistinctTickStrings, yValueScaleFunction, } from './VAFChartUtils'; import { GROUP_BY_NONE } from '../timeline2/VAFChartControls'; @@ -940,6 +941,36 @@ describe('VAFChartUtils', () => { }); }); + describe('minimalDistinctTickStrings', () => { + it('works with empty array', () => { + assert.deepEqual(minimalDistinctTickStrings([]), []); + }); + it('converts to string', () => { + assert.deepEqual(minimalDistinctTickStrings([1]), ['1']); + }); + it('deduplicate numbers', () => { + assert.deepEqual(minimalDistinctTickStrings([1, 1]), ['1']); + }); + it('shows equals digits in fractional part', () => { + assert.deepEqual(minimalDistinctTickStrings([1, 1.1]), [ + '1.0', + '1.1', + ]); + }); + it('shows just enough numbers in fractional part to distinguish number', () => { + assert.deepEqual( + minimalDistinctTickStrings([0.01, 0.002, 0.0003]), + ['0.010', '0.002', '0.000'] + ); + }); + it('falls back on the scientific notation of original numbers if 3 decimal digits are not enough to distinguish', () => { + assert.deepEqual(minimalDistinctTickStrings([0.0001, 0.000201]), [ + '1e-4', + '2.01e-4', + ]); + }); + }); + describe('yValueScaleFunction', () => { it('handles linear scale, zero minY tickmark', () => { const yPadding = 10; diff --git a/src/pages/patientView/timeline2/VAFChartUtils.ts b/src/pages/patientView/timeline2/VAFChartUtils.ts index 04c66b61154..88f4c548bae 100644 --- a/src/pages/patientView/timeline2/VAFChartUtils.ts +++ b/src/pages/patientView/timeline2/VAFChartUtils.ts @@ -329,3 +329,31 @@ export function numLeadingDecimalZeros(y: number) { if (y == 0 || y >= 0.1) return 0; return numberOfLeadingDecimalZeros(y); } + +/** + * Try to return tick labels with fractional part just long enogh to disttinguish them. + * It tries up to 3th positions in fractional part. Otherwise return scientific representation of original numbers. + * Function returns distinct labels. + * If input contains duplicates function deduplicates and return less labels that amount of input numbers. + * @param nums array of ticks as numbers + */ +export function minimalDistinctTickStrings(nums: number[]): string[] { + const distinctNums = nums.filter((v, i, a) => a.indexOf(v) === i); + + const fractionalNumbersToShow = distinctNums.map(num => + Number.isInteger(num) ? 0 : numLeadingDecimalZeros(num) + 1 + ); + + const fromPos = Math.min(...fractionalNumbersToShow); + const toPos = 3; + + for (let pos = fromPos; pos <= toPos; pos++) { + const labels = distinctNums + .map(num => num.toFixed(pos)) + .filter((v, i, a) => a.indexOf(v) === i); + if (labels.length === distinctNums.length) { + return labels; + } + } + return distinctNums.map(num => num.toExponential()); +} diff --git a/src/pages/patientView/timeline2/VAFChartWrapper.tsx b/src/pages/patientView/timeline2/VAFChartWrapper.tsx index 5fc9d89a02b..329d7eae89a 100644 --- a/src/pages/patientView/timeline2/VAFChartWrapper.tsx +++ b/src/pages/patientView/timeline2/VAFChartWrapper.tsx @@ -33,6 +33,7 @@ import { IPoint, numLeadingDecimalZeros, yValueScaleFunction, + minimalDistinctTickStrings, } from './VAFChartUtils'; import { VAFChartHeader } from './VAFChartHeader'; import { @@ -123,14 +124,18 @@ export default class VAFChartWrapper extends React.Component< } @computed get ticks(): { label: string; value: number; offset: number }[] { - const tickmarkValues = getYAxisTickmarks( + let tickmarkValues = getYAxisTickmarks( this.minYTickmarkValue, this.maxYTickmarkValue ); - const numDecimals = numLeadingDecimalZeros(this.minYTickmarkValue) + 1; - return _.map(tickmarkValues, (v: number) => { + const labels = minimalDistinctTickStrings(tickmarkValues); + const ticksHasDuplicates = tickmarkValues.length !== labels.length; + if (ticksHasDuplicates) { + tickmarkValues = labels.map(label => Number(label)); + } + return _.map(tickmarkValues, (v: number, indx: number) => { return { - label: v.toFixed(numDecimals), + label: labels[indx], value: v, offset: this.scaleYValue(v), }; From 2eeb33481c1cb06fec6af4b7e50b9e232c3fdff1 Mon Sep 17 00:00:00 2001 From: pnp300 Date: Tue, 27 Oct 2020 08:59:22 +0100 Subject: [PATCH 2/2] Fix trigger of rerender when logScale selected --- .../patientView/timeline2/VAFChartWrapper.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/pages/patientView/timeline2/VAFChartWrapper.tsx b/src/pages/patientView/timeline2/VAFChartWrapper.tsx index 329d7eae89a..087dc840ac5 100644 --- a/src/pages/patientView/timeline2/VAFChartWrapper.tsx +++ b/src/pages/patientView/timeline2/VAFChartWrapper.tsx @@ -417,11 +417,13 @@ export default class VAFChartWrapper extends React.Component< return _.sum([this.wrapperStore.dataHeight, footerHeight]); } - render() { - if (!this.store || !this.wrapperStore) return null; - + @computed get customTracks() { const mouseOverMutation = this.props.dataStore.mouseOverMutation; const selectedMutations = this.props.dataStore.selectedMutations; + const lineData = this.scaledAndColoredLineData; + const height = this.vafChartHeight; + const headerWidth = + this.numGroupByGroups > 0 ? 150 : this.props.headerWidth; const vafPlotTrack = { renderHeader: (store: TimelineStore) => ( @@ -443,17 +445,21 @@ export default class VAFChartWrapper extends React.Component< onlyShowSelectedInVAFChart={ this.wrapperStore.onlyShowSelectedInVAFChart } - lineData={this.scaledAndColoredLineData} - height={this.vafChartHeight} + lineData={lineData} + height={height} width={this.store.pixelWidth} /> ), disableHover: true, - height: (store: TimelineStore) => this.vafChartHeight, + height: (store: TimelineStore) => height, labelForExport: 'VAF', } as CustomTrackSpecification; - let customTracks = [vafPlotTrack].concat(this.sampleIconsTracks); + return [vafPlotTrack].concat(this.sampleIconsTracks); + } + + render() { + if (!this.store || !this.wrapperStore) return null; return ( <> @@ -476,7 +482,7 @@ export default class VAFChartWrapper extends React.Component< hideLabels={false} hideXAxis={this.wrapperStore.showSequentialMode} visibleTracks={[]} - customTracks={customTracks} + customTracks={this.customTracks} headerWidth={ this.numGroupByGroups > 0 ? 150