diff --git a/src/lib/utils/scales/scale_band.test.ts b/src/lib/utils/scales/scale_band.test.ts index be46bb2222..261ec51bc5 100644 --- a/src/lib/utils/scales/scale_band.test.ts +++ b/src/lib/utils/scales/scale_band.test.ts @@ -1,6 +1,6 @@ import { ScaleBand } from './scale_band'; -describe.only('Scale Band', () => { +describe('Scale Band', () => { it('shall clone domain and range arrays', () => { const domain = [0, 1, 2, 3]; const range = [0, 100] as [number, number]; @@ -79,7 +79,12 @@ describe.only('Scale Band', () => { expect(scale2.scale(3)).toBe(81.25); // an empty 1/2 step place at the end }); - test('shall invert all values in range', () => { + it('shall not scale scale null values', () => { + const scale = new ScaleBand([0, 1, 2], [0, 120], undefined, 0.5); + expect(scale.scale(-1)).toBeUndefined(); + expect(scale.scale(3)).toBeUndefined(); + }); + it('shall invert all values in range', () => { const domain = ['a', 'b', 'c', 'd']; const minRange = 0; const maxRange = 100; diff --git a/src/lib/utils/scales/scale_band.ts b/src/lib/utils/scales/scale_band.ts index 740bfb0f82..1c74f9c41b 100644 --- a/src/lib/utils/scales/scale_band.ts +++ b/src/lib/utils/scales/scale_band.ts @@ -1,6 +1,5 @@ import { scaleBand, scaleQuantize, ScaleQuantize } from 'd3-scale'; import { clamp } from '../commons'; -import { StepType } from './scale_continuous'; import { ScaleType } from './scales'; import { Scale } from './scales'; @@ -61,7 +60,7 @@ export class ScaleBand implements Scale { invert(value: any) { return this.invertedScale(value); } - invertWithStep(value: any, stepType?: StepType) { + invertWithStep(value: any) { return this.invertedScale(value); } } diff --git a/src/lib/utils/scales/scale_continuous.test.ts b/src/lib/utils/scales/scale_continuous.test.ts index 00b33e4b43..113facc755 100644 --- a/src/lib/utils/scales/scale_continuous.test.ts +++ b/src/lib/utils/scales/scale_continuous.test.ts @@ -1,11 +1,14 @@ import { DateTime } from 'luxon'; +import { XDomain } from '../../series/domains/x_domain'; +import { computeXScale } from '../../series/scales'; +import { Domain } from '../domain'; import { ScaleBand } from './scale_band'; import { isLogarithmicScale, ScaleContinuous } from './scale_continuous'; import { ScaleType } from './scales'; describe('Scale Continuous', () => { test('shall invert on continuous scale linear', () => { - const domain = [0, 2]; + const domain: Domain = [0, 2]; const minRange = 0; const maxRange = 100; const scale = new ScaleContinuous(ScaleType.Linear, domain, [minRange, maxRange]); @@ -26,7 +29,7 @@ describe('Scale Continuous', () => { expect(scale.invert(100)).toBe(endTime.toMillis()); }); test('check if a scale is log scale', () => { - const domain = [0, 2]; + const domain: Domain = [0, 2]; const range: [number, number] = [0, 100]; const scaleLinear = new ScaleContinuous(ScaleType.Linear, domain, range); const scaleLog = new ScaleContinuous(ScaleType.Log, domain, range); @@ -39,4 +42,101 @@ describe('Scale Continuous', () => { expect(isLogarithmicScale(scaleSqrt)).toBe(false); expect(isLogarithmicScale(scaleBand)).toBe(false); }); + test('can get the right x value on linear scale', () => { + const domain: Domain = [0, 2]; + const data = [0, 0.5, 0.8, 2]; + const range: [number, number] = [0, 2]; + const scaleLinear = new ScaleContinuous(ScaleType.Linear, domain, range); + expect(scaleLinear.bandwidth).toBe(0); + expect(scaleLinear.invertWithStep(0, data)).toBe(0); + expect(scaleLinear.invertWithStep(0.1, data)).toBe(0); + + expect(scaleLinear.invertWithStep(0.4, data)).toBe(0.5); + expect(scaleLinear.invertWithStep(0.5, data)).toBe(0.5); + expect(scaleLinear.invertWithStep(0.6, data)).toBe(0.5); + + expect(scaleLinear.invertWithStep(0.7, data)).toBe(0.8); + expect(scaleLinear.invertWithStep(0.8, data)).toBe(0.8); + expect(scaleLinear.invertWithStep(0.9, data)).toBe(0.8); + + expect(scaleLinear.invertWithStep(2, data)).toBe(2); + + expect(scaleLinear.invertWithStep(1.7, data)).toBe(2); + + expect(scaleLinear.invertWithStep(0.8 + (2 - 0.8) / 2, data)).toBe(0.8); + expect(scaleLinear.invertWithStep(0.8 + (2 - 0.8) / 2 - 0.01, data)).toBe(0.8); + + expect(scaleLinear.invertWithStep(0.8 + (2 - 0.8) / 2 + 0.01, data)).toBe(2); + }); + test('invert with step x value on linear band scale', () => { + const data = [0, 1, 2]; + const xDomain: XDomain = { + domain: [0, 2], + isBandScale: true, + minInterval: 1, + scaleType: ScaleType.Linear, + type: 'xDomain', + }; + + const scaleLinear = computeXScale(xDomain, 1, 0, 120, 0); + expect(scaleLinear.bandwidth).toBe(40); + expect(scaleLinear.invertWithStep(0, data)).toBe(0); + expect(scaleLinear.invertWithStep(40, data)).toBe(1); + + expect(scaleLinear.invertWithStep(41, data)).toBe(1); + expect(scaleLinear.invertWithStep(79, data)).toBe(1); + + expect(scaleLinear.invertWithStep(80, data)).toBe(2); + expect(scaleLinear.invertWithStep(81, data)).toBe(2); + expect(scaleLinear.invertWithStep(120, data)).toBe(2); + }); + test('can get the right x value on linear scale with regular band 1', () => { + const domain = [0, 100]; + const data = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]; + + // we tweak the maxRange removing the bandwidth to correctly compute + // a band linear scale in computeXScale + const range: [number, number] = [0, 100 - 10]; + const scaleLinear = new ScaleContinuous(ScaleType.Linear, domain, range, 10, 10); + expect(scaleLinear.bandwidth).toBe(10); + expect(scaleLinear.invertWithStep(0, data)).toBe(0); + expect(scaleLinear.invertWithStep(10, data)).toBe(10); + expect(scaleLinear.invertWithStep(20, data)).toBe(20); + expect(scaleLinear.invertWithStep(90, data)).toBe(90); + }); + test('can get the right x value on linear scale with band', () => { + const data = [0, 10, 20, 50, 90]; + // we tweak the maxRange removing the bandwidth to correctly compute + // a band linear scale in computeXScale + + const xDomain: XDomain = { + domain: [0, 100], + isBandScale: true, + minInterval: 10, + scaleType: ScaleType.Linear, + type: 'xDomain', + }; + + const scaleLinear = computeXScale(xDomain, 1, 0, 110, 0); + // const scaleLinear = new ScaleContinuous(ScaleType.Linear, domain, range, 10, 10); + expect(scaleLinear.bandwidth).toBe(10); + + expect(scaleLinear.invertWithStep(0, data)).toBe(0); + expect(scaleLinear.invertWithStep(5, data)).toBe(0); + expect(scaleLinear.invertWithStep(9, data)).toBe(0); + + expect(scaleLinear.invertWithStep(10, data)).toBe(10); + expect(scaleLinear.invertWithStep(11, data)).toBe(10); + expect(scaleLinear.invertWithStep(19, data)).toBe(10); + + expect(scaleLinear.invertWithStep(20, data)).toBe(20); + expect(scaleLinear.invertWithStep(21, data)).toBe(20); + expect(scaleLinear.invertWithStep(25, data)).toBe(20); + expect(scaleLinear.invertWithStep(29, data)).toBe(20); + expect(scaleLinear.invertWithStep(30, data)).toBe(20); + expect(scaleLinear.invertWithStep(39, data)).toBe(20); + expect(scaleLinear.invertWithStep(40, data)).toBe(50); + expect(scaleLinear.invertWithStep(50, data)).toBe(50); + expect(scaleLinear.invertWithStep(90, data)).toBe(90); + }); }); diff --git a/src/lib/utils/scales/scale_continuous.ts b/src/lib/utils/scales/scale_continuous.ts index 006cb2ef45..5524f17b84 100644 --- a/src/lib/utils/scales/scale_continuous.ts +++ b/src/lib/utils/scales/scale_continuous.ts @@ -1,3 +1,4 @@ +import { bisectLeft } from 'd3-array'; import { scaleLinear, scaleLog, scaleSqrt, scaleUtc } from 'd3-scale'; import { DateTime } from 'luxon'; import { clamp } from '../commons'; @@ -60,11 +61,6 @@ export function limitLogScaleDomain(domain: any[]) { } return domain; } -export enum StepType { - StepBefore = 'before', - StepAfter = 'after', - Step = 'half', -} export class ScaleContinuous implements Scale { readonly bandwidth: number; @@ -149,7 +145,7 @@ export class ScaleContinuous implements Scale { }); } else { if (this.minInterval > 0) { - const intervalCount = (this.domain[1] - this.domain[0]) / this.minInterval; + const intervalCount = Math.floor((this.domain[1] - this.domain[0]) / this.minInterval); this.tickValues = new Array(intervalCount + 1).fill(0).map((d, i) => { return this.domain[0] + i * this.minInterval; }); @@ -173,10 +169,34 @@ export class ScaleContinuous implements Scale { } return invertedValue; } - invertWithStep(value: number, stepType?: StepType) { - const invertedValue = this.invert(value); - const forcedStep = this.bandwidth > 0 ? StepType.StepAfter : stepType; - return invertValue(this.domain[0], invertedValue, this.minInterval, forcedStep); + invertWithStep(value: number, data: number[]): any { + const invertedValue = this.invert(value - this.bandwidth / 2); + const leftIndex = bisectLeft(data, invertedValue); + if (leftIndex === 0) { + // is equal or less than the first value + const prevValue1 = data[leftIndex]; + if (data.length === 0) { + return prevValue1; + } + const nextValue1 = data[leftIndex + 1]; + const nextDiff1 = Math.abs(nextValue1 - invertedValue); + const prevDiff1 = Math.abs(invertedValue - prevValue1); + if (nextDiff1 < prevDiff1) { + return nextValue1; + } + return prevValue1; + } + if (leftIndex === data.length) { + return data[leftIndex - 1]; + } + const nextValue = data[leftIndex]; + const prevValue = data[leftIndex - 1]; + const nextDiff = Math.abs(nextValue - invertedValue); + const prevDiff = Math.abs(invertedValue - prevValue); + if (nextDiff <= prevDiff) { + return nextValue; + } + return prevValue; } } @@ -187,58 +207,3 @@ export function isContinuousScale(scale: Scale): scale is ScaleContinuous { export function isLogarithmicScale(scale: Scale) { return scale.type === ScaleType.Log; } - -function invertValue( - domainMin: number, - invertedValue: number, - minInterval: number, - stepType?: StepType, -) { - if (minInterval > 0) { - switch (stepType) { - case StepType.StepAfter: - return linearStepAfter(invertedValue, minInterval); - case StepType.StepBefore: - return linearStepBefore(invertedValue, minInterval); - case StepType.Step: - default: - return linearStep(domainMin, invertedValue, minInterval); - } - } - return invertedValue; -} - -/** - * Return an inverted value that is valid from the exact point of the scale - * till the end of the interval. |--------|********| - * @param invertedValue the inverted value - * @param minInterval the data minimum interval grether than 0 - */ -export function linearStepAfter(invertedValue: number, minInterval: number): number { - return Math.floor(invertedValue / minInterval) * minInterval; -} - -/** - * Return an inverted value that is valid from the half point before and half point - * after the value. |----****|*****----| - * till the end of the interval. - * @param domainMin the domain's minimum value - * @param invertedValue the inverted value - * @param minInterval the data minimum interval grether than 0 - */ -export function linearStep(domainMin: number, invertedValue: number, minInterval: number): number { - const diff = (invertedValue - domainMin) / minInterval; - const base = diff - Math.floor(diff) > 0.5 ? 1 : 0; - return domainMin + Math.floor(diff) * minInterval + minInterval * base; -} - -/** - * Return an inverted value that is valid from the half point before and half point - * after the value. |********|--------| - * till the end of the interval. - * @param invertedValue the inverted value - * @param minInterval the data minimum interval grether than 0 - */ -export function linearStepBefore(invertedValue: number, minInterval: number): number { - return Math.ceil(invertedValue / minInterval) * minInterval; -} diff --git a/src/lib/utils/scales/scale_time.test.ts b/src/lib/utils/scales/scale_time.test.ts index 6231f94efb..13e48f01e7 100644 --- a/src/lib/utils/scales/scale_time.test.ts +++ b/src/lib/utils/scales/scale_time.test.ts @@ -71,6 +71,7 @@ describe('[Scale Time] - timezones', () => { const startTime = DateTime.fromISO('2019-01-01T00:00:00.000').toMillis(); const midTime = DateTime.fromISO('2019-01-02T00:00:00.000').toMillis(); const endTime = DateTime.fromISO('2019-01-03T00:00:00.000').toMillis(); + const data = [startTime, midTime, endTime]; const domain = [startTime, endTime]; const minRange = 0; const maxRange = 100; @@ -86,13 +87,13 @@ describe('[Scale Time] - timezones', () => { expect(scale.invert(0)).toBe(startTime); expect(scale.invert(50)).toBe(midTime); expect(scale.invert(100)).toBe(endTime); - expect(scale.invertWithStep(0)).toBe(startTime); - expect(scale.invertWithStep(25)).toBe(startTime); - expect(scale.invertWithStep(26)).toBe(midTime); - expect(scale.invertWithStep(50)).toBe(midTime); - expect(scale.invertWithStep(75)).toBe(midTime); - expect(scale.invertWithStep(76)).toBe(endTime); - expect(scale.invertWithStep(100)).toBe(endTime); + expect(scale.invertWithStep(0, data)).toBe(startTime); + expect(scale.invertWithStep(24, data)).toBe(startTime); + expect(scale.invertWithStep(25, data)).toBe(midTime); + expect(scale.invertWithStep(50, data)).toBe(midTime); + expect(scale.invertWithStep(74, data)).toBe(midTime); + expect(scale.invertWithStep(76, data)).toBe(endTime); + expect(scale.invertWithStep(100, data)).toBe(endTime); expect(scale.tickValues.length).toBe(9); expect(scale.tickValues[0]).toEqual(startTime); expect(scale.tickValues[4]).toEqual(midTime); @@ -102,6 +103,7 @@ describe('[Scale Time] - timezones', () => { const startTime = DateTime.fromISO('2019-01-01T00:00:00.000Z').toMillis(); const midTime = DateTime.fromISO('2019-01-02T00:00:00.000Z').toMillis(); const endTime = DateTime.fromISO('2019-01-03T00:00:00.000Z').toMillis(); + const data = [startTime, midTime, endTime]; const domain = [startTime, endTime]; const minRange = 0; const maxRange = 100; @@ -117,13 +119,13 @@ describe('[Scale Time] - timezones', () => { expect(scale.invert(0)).toBe(startTime); expect(scale.invert(50)).toBe(midTime); expect(scale.invert(100)).toBe(endTime); - expect(scale.invertWithStep(0)).toBe(startTime); - expect(scale.invertWithStep(25)).toBe(startTime); - expect(scale.invertWithStep(26)).toBe(midTime); - expect(scale.invertWithStep(50)).toBe(midTime); - expect(scale.invertWithStep(75)).toBe(midTime); - expect(scale.invertWithStep(76)).toBe(endTime); - expect(scale.invertWithStep(100)).toBe(endTime); + expect(scale.invertWithStep(0, data)).toBe(startTime); + expect(scale.invertWithStep(24, data)).toBe(startTime); + expect(scale.invertWithStep(25, data)).toBe(midTime); + expect(scale.invertWithStep(50, data)).toBe(midTime); + expect(scale.invertWithStep(74, data)).toBe(midTime); + expect(scale.invertWithStep(75, data)).toBe(endTime); + expect(scale.invertWithStep(100, data)).toBe(endTime); expect(scale.tickValues.length).toBe(9); expect(scale.tickValues[0]).toEqual(startTime); expect(scale.tickValues[4]).toEqual(midTime); @@ -133,6 +135,7 @@ describe('[Scale Time] - timezones', () => { const startTime = DateTime.fromISO('2019-01-01T00:00:00.000+08:00').toMillis(); const midTime = DateTime.fromISO('2019-01-02T00:00:00.000+08:00').toMillis(); const endTime = DateTime.fromISO('2019-01-03T00:00:00.000+08:00').toMillis(); + const data = [startTime, midTime, endTime]; const domain = [startTime, endTime]; const minRange = 0; const maxRange = 100; @@ -148,13 +151,13 @@ describe('[Scale Time] - timezones', () => { expect(scale.invert(0)).toBe(startTime); expect(scale.invert(50)).toBe(midTime); expect(scale.invert(100)).toBe(endTime); - expect(scale.invertWithStep(0)).toBe(startTime); - expect(scale.invertWithStep(25)).toBe(startTime); - expect(scale.invertWithStep(26)).toBe(midTime); - expect(scale.invertWithStep(50)).toBe(midTime); - expect(scale.invertWithStep(75)).toBe(midTime); - expect(scale.invertWithStep(76)).toBe(endTime); - expect(scale.invertWithStep(100)).toBe(endTime); + expect(scale.invertWithStep(0, data)).toBe(startTime); + expect(scale.invertWithStep(24, data)).toBe(startTime); + expect(scale.invertWithStep(25, data)).toBe(midTime); + expect(scale.invertWithStep(50, data)).toBe(midTime); + expect(scale.invertWithStep(74, data)).toBe(midTime); + expect(scale.invertWithStep(75, data)).toBe(endTime); + expect(scale.invertWithStep(100, data)).toBe(endTime); expect(scale.tickValues.length).toBe(9); expect(scale.tickValues[0]).toEqual(startTime); expect(scale.tickValues[4]).toEqual(midTime); @@ -164,6 +167,7 @@ describe('[Scale Time] - timezones', () => { const startTime = DateTime.fromISO('2019-01-01T00:00:00.000-08:00').toMillis(); const midTime = DateTime.fromISO('2019-01-02T00:00:00.000-08:00').toMillis(); const endTime = DateTime.fromISO('2019-01-03T00:00:00.000-08:00').toMillis(); + const data = [startTime, midTime, endTime]; const domain = [startTime, endTime]; const minRange = 0; const maxRange = 100; @@ -179,13 +183,13 @@ describe('[Scale Time] - timezones', () => { expect(scale.invert(0)).toBe(startTime); expect(scale.invert(50)).toBe(midTime); expect(scale.invert(100)).toBe(endTime); - expect(scale.invertWithStep(0)).toBe(startTime); - expect(scale.invertWithStep(25)).toBe(startTime); - expect(scale.invertWithStep(26)).toBe(midTime); - expect(scale.invertWithStep(50)).toBe(midTime); - expect(scale.invertWithStep(75)).toBe(midTime); - expect(scale.invertWithStep(76)).toBe(endTime); - expect(scale.invertWithStep(100)).toBe(endTime); + expect(scale.invertWithStep(0, data)).toBe(startTime); + expect(scale.invertWithStep(24, data)).toBe(startTime); + expect(scale.invertWithStep(25, data)).toBe(midTime); + expect(scale.invertWithStep(50, data)).toBe(midTime); + expect(scale.invertWithStep(74, data)).toBe(midTime); + expect(scale.invertWithStep(75, data)).toBe(endTime); + expect(scale.invertWithStep(100, data)).toBe(endTime); expect(scale.tickValues.length).toBe(9); expect(scale.tickValues[0]).toEqual(startTime); expect(scale.tickValues[4]).toEqual(midTime); @@ -199,6 +203,7 @@ describe('[Scale Time] - timezones', () => { }).toMillis(); const midTime = DateTime.fromISO('2019-01-02T00:00:00.000', { zone: timezone }).toMillis(); const endTime = DateTime.fromISO('2019-01-03T00:00:00.000', { zone: timezone }).toMillis(); + const data = [startTime, midTime, endTime]; const domain = [startTime, endTime]; const minRange = 0; const maxRange = 100; @@ -217,13 +222,13 @@ describe('[Scale Time] - timezones', () => { expect(scale.invert(0)).toBe(startTime); expect(scale.invert(50)).toBe(midTime); expect(scale.invert(100)).toBe(endTime); - expect(scale.invertWithStep(0)).toBe(startTime); - expect(scale.invertWithStep(25)).toBe(startTime); - expect(scale.invertWithStep(26)).toBe(midTime); - expect(scale.invertWithStep(50)).toBe(midTime); - expect(scale.invertWithStep(75)).toBe(midTime); - expect(scale.invertWithStep(76)).toBe(endTime); - expect(scale.invertWithStep(100)).toBe(endTime); + expect(scale.invertWithStep(0, data)).toBe(startTime); + expect(scale.invertWithStep(24, data)).toBe(startTime); + expect(scale.invertWithStep(25, data)).toBe(midTime); + expect(scale.invertWithStep(50, data)).toBe(midTime); + expect(scale.invertWithStep(74, data)).toBe(midTime); + expect(scale.invertWithStep(75, data)).toBe(endTime); + expect(scale.invertWithStep(100, data)).toBe(endTime); expect(scale.tickValues.length).toBe(9); expect(scale.tickValues[0]).toEqual(startTime); expect(scale.tickValues[4]).toEqual(midTime); diff --git a/src/lib/utils/scales/scales.test.ts b/src/lib/utils/scales/scales.test.ts index b45de0c24e..97fac45698 100644 --- a/src/lib/utils/scales/scales.test.ts +++ b/src/lib/utils/scales/scales.test.ts @@ -142,25 +142,26 @@ describe('Scale Test', () => { }); test('compare ordinal scale and linear/band invertWithStep 3 bars', () => { - const dataLinear = [0, 2]; - const dataOrdinal = [0, 1, 2]; + const data = [0, 1, 2]; + const domainLinear = [0, 2]; + const domainOrdinal = [0, 1, 2]; const minRange = 0; const maxRange = 120; const bandwidth = maxRange / 3; const linearScale = new ScaleContinuous( ScaleType.Linear, - dataLinear, + domainLinear, [minRange, maxRange - bandwidth], // we currently limit the range like that a band linear scale bandwidth, 1, ); - const ordinalScale = new ScaleBand(dataOrdinal, [minRange, maxRange]); + const ordinalScale = new ScaleBand(domainOrdinal, [minRange, maxRange]); expect(ordinalScale.invertWithStep(0)).toBe(0); expect(ordinalScale.invertWithStep(40)).toBe(1); expect(ordinalScale.invertWithStep(80)).toBe(2); - expect(linearScale.invertWithStep(0)).toBe(0); - expect(linearScale.invertWithStep(40)).toBe(1); - expect(linearScale.invertWithStep(80)).toBe(2); + expect(linearScale.invertWithStep(0, data)).toBe(0); + expect(linearScale.invertWithStep(40, data)).toBe(1); + expect(linearScale.invertWithStep(80, data)).toBe(2); }); test('compare ordinal scale and linear/band 2 bars', () => { const dataLinear = [0, 1]; @@ -185,29 +186,10 @@ describe('Scale Test', () => { expect(ordinalScale.invertWithStep(0)).toBe(0); expect(ordinalScale.invertWithStep(50)).toBe(1); expect(ordinalScale.invertWithStep(100)).toBe(1); - expect(linearScale.invertWithStep(0)).toBe(0); - expect(linearScale.invertWithStep(50)).toBe(1); - // expect(linearScale.invertWithStep(100)).toBe(1); + expect(linearScale.invertWithStep(0, dataLinear)).toBe(0); + expect(linearScale.invertWithStep(50, dataLinear)).toBe(1); + expect(linearScale.invertWithStep(100, dataLinear)).toBe(1); expect(linearScale.bandwidth).toBe(50); expect(linearScale.range).toEqual([0, 50]); }); - test.skip('compare ordinal scale and linear/band 2 bars', () => { - // I'm skipping this because the current implementation of linearScale - // is a bit different than the ordinalScale when forcing bandwidth on linear scale - const dataLinear = [0, 1]; - const dataOrdinal = [0, 1]; - const minRange = 0; - const maxRange = 100; - const bandwidth = maxRange / 2; - const linearScale = new ScaleContinuous( - ScaleType.Linear, - dataLinear, - [minRange, maxRange - bandwidth], // we currently limit the range like that a band linear scale - bandwidth, - 1, - ); - const ordinalScale = new ScaleBand(dataOrdinal, [minRange, maxRange]); - expect(ordinalScale.invertWithStep(100)).toBe(1); - expect(linearScale.invertWithStep(100)).toBe(1); - }); }); diff --git a/src/lib/utils/scales/scales.ts b/src/lib/utils/scales/scales.ts index 584f623ea0..59ca386cd7 100644 --- a/src/lib/utils/scales/scales.ts +++ b/src/lib/utils/scales/scales.ts @@ -1,12 +1,10 @@ -import { StepType } from './scale_continuous'; - export interface Scale { domain: any[]; range: number[]; ticks: () => any[]; scale: (value: any) => number; invert: (value: number) => any; - invertWithStep: (value: number, stepType?: StepType) => any; + invertWithStep: (value: number, data: any[]) => any; bandwidth: number; minInterval: number; type: ScaleType; diff --git a/src/state/chart_state.timescales.test.ts b/src/state/chart_state.timescales.test.ts index 5d179d0d16..d6ce56065e 100644 --- a/src/state/chart_state.timescales.test.ts +++ b/src/state/chart_state.timescales.test.ts @@ -146,19 +146,20 @@ describe('Render chart', () => { expect(store.geometries!.lines[0].points.length).toBe(3); }); test('check scale values', () => { + const xValues = [date1, date2, date3]; expect(store.xScale!.minInterval).toBe(1000 * 60 * 5); expect(store.xScale!.domain).toEqual([date1, date3]); expect(store.xScale!.range).toEqual([0, 100]); expect(store.xScale!.invert(0)).toBe(date1); expect(store.xScale!.invert(50)).toBe(date2); expect(store.xScale!.invert(100)).toBe(date3); - expect(store.xScale!.invertWithStep(5)).toBe(date1); - expect(store.xScale!.invertWithStep(20)).toBe(date1); - expect(store.xScale!.invertWithStep(30)).toBe(date2); - expect(store.xScale!.invertWithStep(50)).toBe(date2); - expect(store.xScale!.invertWithStep(70)).toBe(date2); - expect(store.xScale!.invertWithStep(80)).toBe(date3); - expect(store.xScale!.invertWithStep(100)).toBe(date3); + expect(store.xScale!.invertWithStep(5, xValues)).toBe(date1); + expect(store.xScale!.invertWithStep(20, xValues)).toBe(date1); + expect(store.xScale!.invertWithStep(30, xValues)).toBe(date2); + expect(store.xScale!.invertWithStep(50, xValues)).toBe(date2); + expect(store.xScale!.invertWithStep(70, xValues)).toBe(date2); + expect(store.xScale!.invertWithStep(80, xValues)).toBe(date3); + expect(store.xScale!.invertWithStep(100, xValues)).toBe(date3); }); test('check mouse position correctly return inverted value', () => { store.setCursorPosition(15, 10); // check first valid tooltip diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 3b91ef02ab..47a0bf8fc6 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -45,6 +45,7 @@ import { import { formatTooltip, getSeriesTooltipValues } from '../lib/series/tooltip'; import { LIGHT_THEME } from '../lib/themes/light_theme'; import { mergeWithDefaultAnnotationLine, mergeWithDefaultAnnotationRect, Theme } from '../lib/themes/theme'; +import { compareByValueAsc } from '../lib/utils/commons'; import { computeChartDimensions, Dimensions } from '../lib/utils/dimensions'; import { Domain } from '../lib/utils/domain'; import { AnnotationId, AxisId, GroupId, SpecId } from '../lib/utils/ids'; @@ -210,6 +211,7 @@ export class ChartStore { } | null = null; geometriesIndex: Map = new Map(); + geometriesIndexKeys: any[] = []; highlightedGeometries = observable.array([], { deep: false }); animateData = false; @@ -282,7 +284,8 @@ export class ChartStore { } // invert the cursor position to get the scale value - const xValue = this.xScale.invertWithStep(xAxisCursorPosition); + + const xValue = this.xScale.invertWithStep(xAxisCursorPosition, this.geometriesIndexKeys); // update che cursorBandPosition based on chart configuration const isLineAreaOnly = isLineAreaOnlyChart(this.seriesSpecs); @@ -292,6 +295,7 @@ export class ChartStore { this.cursorPosition, this.isTooltipSnapEnabled.get(), this.xScale, + this.geometriesIndexKeys, isLineAreaOnly ? 1 : this.totalBarsInCluster, ); if (updatedCursorBand === undefined) { @@ -871,6 +875,7 @@ export class ChartStore { this.xScale = seriesGeometries.scales.xScale; this.yScales = seriesGeometries.scales.yScales; this.geometriesIndex = seriesGeometries.geometriesIndex; + this.geometriesIndexKeys = [...this.geometriesIndex.keys()].sort(compareByValueAsc); // // compute visible ticks and their positions const axisTicksPositions = getAxisTicksPositions( diff --git a/src/state/crosshair_utils.linear_snap.test.ts b/src/state/crosshair_utils.linear_snap.test.ts index 5dcad57ab9..e3fc3dfbcf 100644 --- a/src/state/crosshair_utils.linear_snap.test.ts +++ b/src/state/crosshair_utils.linear_snap.test.ts @@ -201,6 +201,7 @@ describe('Crosshair utils linear scale', () => { { x: 200, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -210,6 +211,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 200 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -220,6 +222,7 @@ describe('Crosshair utils linear scale', () => { { x: -1, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -230,6 +233,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: -1 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -249,6 +253,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -266,6 +271,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 45 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -283,6 +289,7 @@ describe('Crosshair utils linear scale', () => { { x: 40, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -300,6 +307,7 @@ describe('Crosshair utils linear scale', () => { { x: 90, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -317,6 +325,7 @@ describe('Crosshair utils linear scale', () => { { x: 200, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -333,6 +342,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -350,6 +360,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 45 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -367,6 +378,7 @@ describe('Crosshair utils linear scale', () => { { x: 20, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -384,6 +396,7 @@ describe('Crosshair utils linear scale', () => { { x: 40, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -401,6 +414,7 @@ describe('Crosshair utils linear scale', () => { { x: 95, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -418,6 +432,7 @@ describe('Crosshair utils linear scale', () => { { x: 200, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -435,6 +450,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -452,6 +468,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 45 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -469,6 +486,7 @@ describe('Crosshair utils linear scale', () => { { x: 40, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -486,6 +504,7 @@ describe('Crosshair utils linear scale', () => { { x: 90, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -503,6 +522,7 @@ describe('Crosshair utils linear scale', () => { { x: 200, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -520,6 +540,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -537,6 +558,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 45 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -554,6 +576,7 @@ describe('Crosshair utils linear scale', () => { { x: 20, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -571,6 +594,7 @@ describe('Crosshair utils linear scale', () => { { x: 40, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -588,6 +612,7 @@ describe('Crosshair utils linear scale', () => { { x: 95, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -605,6 +630,7 @@ describe('Crosshair utils linear scale', () => { { x: 200, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -622,6 +648,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -639,6 +666,7 @@ describe('Crosshair utils linear scale', () => { { x: 45, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -656,6 +684,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 40 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -673,6 +702,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 90 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -690,6 +720,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 200 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -707,6 +738,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -724,6 +756,7 @@ describe('Crosshair utils linear scale', () => { { x: 45, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -741,6 +774,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 20 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -758,6 +792,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 40 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -775,6 +810,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 95 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -792,6 +828,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 200 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -809,6 +846,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -826,6 +864,7 @@ describe('Crosshair utils linear scale', () => { { x: 45, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -843,6 +882,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 40 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -860,6 +900,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 90 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -877,6 +918,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 200 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -894,6 +936,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -911,6 +954,7 @@ describe('Crosshair utils linear scale', () => { { x: 45, y: 0 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -928,6 +972,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 20 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -945,6 +990,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 40 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -962,6 +1008,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 95 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -979,6 +1026,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 200 }, snapPosition, lineSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -999,6 +1047,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1016,6 +1065,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 45 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1027,12 +1077,28 @@ describe('Crosshair utils linear scale', () => { }); test('increase of x axis increase the left param 1', () => { - const bandPosition = getCursorBandPosition( + let bandPosition = getCursorBandPosition( + chartRotation, + chartDimensions, + { x: 39, y: 0 }, + snapPosition, + barSeriesScale, + [0, 1, 2], + 1, + ); + expect(bandPosition).toEqual({ + left: 0, + top: 0, + height: 100, + width: 40, + }); + bandPosition = getCursorBandPosition( chartRotation, chartDimensions, { x: 40, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1050,6 +1116,7 @@ describe('Crosshair utils linear scale', () => { { x: 90, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1067,6 +1134,7 @@ describe('Crosshair utils linear scale', () => { { x: 200, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -1084,6 +1152,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1101,6 +1170,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 45 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1118,6 +1188,7 @@ describe('Crosshair utils linear scale', () => { { x: 40, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1135,6 +1206,7 @@ describe('Crosshair utils linear scale', () => { { x: 90, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1152,6 +1224,7 @@ describe('Crosshair utils linear scale', () => { { x: 200, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -1169,6 +1242,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1186,6 +1260,7 @@ describe('Crosshair utils linear scale', () => { { x: 45, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1203,6 +1278,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 40 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1220,6 +1296,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 90 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1237,6 +1314,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 200 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); @@ -1254,6 +1332,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1271,6 +1350,7 @@ describe('Crosshair utils linear scale', () => { { x: 45, y: 0 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1288,6 +1368,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 40 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1305,6 +1386,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 90 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toEqual({ @@ -1322,6 +1404,7 @@ describe('Crosshair utils linear scale', () => { { x: 0, y: 200 }, snapPosition, barSeriesScale, + [0, 1, 2], 1, ); expect(bandPosition).toBeUndefined(); diff --git a/src/state/crosshair_utils.ts b/src/state/crosshair_utils.ts index 136895269d..49bea5d426 100644 --- a/src/state/crosshair_utils.ts +++ b/src/state/crosshair_utils.ts @@ -67,6 +67,7 @@ export function getCursorBandPosition( cursorPosition: { x: number; y: number }, snapEnabled: boolean, xScale: Scale, + data: any[], totalBarsInCluster?: number, ): Dimensions | undefined { const { top, left, width, height } = chartDimensions; @@ -75,11 +76,11 @@ export function getCursorBandPosition( return; } const isHorizontalRotated = isHorizontalRotation(chartRotation); - const snappedPosition = getSnapPosition( - xScale.invertWithStep(isHorizontalRotated ? x : y), - xScale, - totalBarsInCluster, - ); + const invertedValue = xScale.invertWithStep(isHorizontalRotated ? x : y, data); + if (invertedValue == null) { + return; + } + const snappedPosition = getSnapPosition(invertedValue, xScale, totalBarsInCluster); if (!snappedPosition) { return; } diff --git a/src/state/test/interactions.test.ts b/src/state/test/interactions.test.ts index 2767209a9d..bb7f410062 100644 --- a/src/state/test/interactions.test.ts +++ b/src/state/test/interactions.test.ts @@ -157,7 +157,7 @@ describe('Chart state pointer interactions', () => { store.yScales = new Map(); store.yScales.set(GROUP_ID, new ScaleContinuous(ScaleType.Linear, [0, 1], [0, 100])); store.geometriesIndex.set(0, [indexedGeom1Red]); - + store.geometriesIndexKeys.push(0); store.tooltipType.set(TooltipType.None); store.setCursorPosition(10, 10 + 70); expect(store.tooltipData).toEqual([]); @@ -165,6 +165,7 @@ describe('Chart state pointer interactions', () => { store.tooltipType.set(TooltipType.Follow); store.setCursorPosition(10, 10 + 70); + expect(store.geometriesIndexKeys.length).toBe(1); expect(store.isTooltipVisible.get()).toBe(true); expect(store.highlightedGeometries.length).toBe(1); }); @@ -198,6 +199,8 @@ function mouseOverTestSuite(scaleType: ScaleType) { store.yScales = yScales; store.geometriesIndex.set(0, [indexedGeom1Red]); store.geometriesIndex.set(1, [indexedGeom2Blue]); + store.geometriesIndexKeys.push(0); + store.geometriesIndexKeys.push(1); onOverListener = jest.fn((elements: GeometryValue[]): undefined => undefined); onOutListener = jest.fn((): undefined => undefined); store.setOnElementOverListener(onOverListener); diff --git a/stories/bar_chart.tsx b/stories/bar_chart.tsx index 736a0a5d13..75b19d80d0 100644 --- a/stories/bar_chart.tsx +++ b/stories/bar_chart.tsx @@ -132,6 +132,42 @@ storiesOf('Bar Chart', module) ); }) + .add('with linear x axis no linear interval', () => { + return ( + + + + Number(d).toFixed(2)} + /> + + + + ); + }) .add('with time x axis', () => { const formatter = timeFormatter(niceTimeFormatByDay(1)); return ( @@ -678,7 +714,7 @@ storiesOf('Bar Chart', module) }) .add('with high data volume', () => { const dg = new DataGenerator(); - const data = dg.generateSimpleSeries(200); + const data = dg.generateSimpleSeries(15000); return (