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), };