Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(scales): use bisect to handle invertWithStep #200

Merged
merged 4 commits into from
May 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/lib/utils/scales/scale_band.test.ts
Original file line number Diff line number Diff line change
@@ -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];
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 1 addition & 2 deletions src/lib/utils/scales/scale_band.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
}
}
Expand Down
104 changes: 102 additions & 2 deletions src/lib/utils/scales/scale_continuous.test.ts
Original file line number Diff line number Diff line change
@@ -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]);
Expand All @@ -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);
Expand All @@ -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);
});
});
95 changes: 30 additions & 65 deletions src/lib/utils/scales/scale_continuous.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
});
Expand All @@ -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;
}
}

Expand All @@ -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;
}
Loading