Skip to content

Commit

Permalink
fix: add domain fitting for tsvb regression (#511)
Browse files Browse the repository at this point in the history
* feat: add domain fitting (#510)

* Add option to fit y domain to data

* Remove story cuz vr tests are failing to run

* fix bad type
  • Loading branch information
nickofthyme committed Jan 10, 2020
1 parent 3301b36 commit bcd08b7
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 32 deletions.
21 changes: 14 additions & 7 deletions src/chart_types/xy_chart/domains/y_domain.ts
Expand Up @@ -72,30 +72,32 @@ function mergeYDomainForGroup(
): YDomain {
const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]);
const { isPercentageStack } = groupSpecs;
const fitToExtent = customDomain && customDomain.fit;

let domain: number[];
if (isPercentageStack) {
domain = computeContinuousDataDomain([0, 1], identity);
domain = computeContinuousDataDomain([0, 1], identity, fitToExtent);
} else {
// compute stacked domain
const isStackedScaleToExtent = groupSpecs.stacked.some((spec) => {
return spec.yScaleToDataExtent;
});
const stackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.stacked);
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent);
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent, fitToExtent);

// compute non stacked domain
const isNonStackedScaleToExtent = groupSpecs.nonStacked.some((spec) => {
return spec.yScaleToDataExtent;
});
const nonStackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.nonStacked);
const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent);
const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent, fitToExtent);

// merge stacked and non stacked domain together
domain = computeContinuousDataDomain(
[...stackedDomain, ...nonStackedDomain],
identity,
isStackedScaleToExtent || isNonStackedScaleToExtent,
fitToExtent,
);

const [computedDomainMin, computedDomainMax] = domain;
Expand Down Expand Up @@ -139,7 +141,11 @@ export function getDataSeriesOnGroup(
);
}

function computeYStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boolean): number[] {
function computeYStackedDomain(
dataseries: RawDataSeries[],
scaleToExtent: boolean,
fitToExtent: boolean = false,
): number[] {
const stackMap = new Map<any, any[]>();
dataseries.forEach((ds, index) => {
ds.data.forEach((datum) => {
Expand All @@ -158,9 +164,10 @@ function computeYStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boole
if (dataValues.length === 0) {
return [];
}
return computeContinuousDataDomain(dataValues, identity, scaleToExtent);
return computeContinuousDataDomain(dataValues, identity, scaleToExtent, fitToExtent);
}
function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boolean) {

function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boolean, fitToExtent: boolean = false) {
const yValues = new Set<any>();
dataseries.forEach((ds) => {
ds.data.forEach((datum) => {
Expand All @@ -173,7 +180,7 @@ function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: bo
if (yValues.size === 0) {
return [];
}
return computeContinuousDataDomain([...yValues.values()], identity, scaleToExtent);
return computeContinuousDataDomain([...yValues.values()], identity, scaleToExtent, fitToExtent);
}
export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) {
const specsByGroupIds = new Map<
Expand Down
27 changes: 19 additions & 8 deletions src/chart_types/xy_chart/utils/specs.ts
Expand Up @@ -42,8 +42,9 @@ export type BarStyleAccessor = (datum: RawDataSeriesDatum, geometryId: GeometryI
export type PointStyleAccessor = (datum: RawDataSeriesDatum, geometryId: GeometryId) => PointStyleOverride;
export const DEFAULT_GLOBAL_ID = '__global__';

interface DomainMinInterval {
/** Custom minInterval for the domain which will affect data bucket size.
interface DomainBase {
/**
* Custom minInterval for the domain which will affect data bucket size.
* The minInterval cannot be greater than the computed minimum interval between any two adjacent data points.
* Further, if you specify a custom numeric minInterval for a timeseries, please note that due to the restriction
* above, the specified numeric minInterval will be interpreted as a fixed interval.
Expand All @@ -52,22 +53,32 @@ interface DomainMinInterval {
* be a valid interval because it is greater than the computed minInterval of 365 days betwen the other years.
*/
minInterval?: number;
/**
* Whether to fit the domain to the data. ONLY applies to `yDomains`
*
* Setting `max` or `min` will override this functionality.
*/
fit?: boolean;
}

interface LowerBound {
/** Lower bound of domain range */
/**
* Lower bound of domain range
*/
min: number;
}

interface UpperBound {
/** Upper bound of domain range */
/**
* Upper bound of domain range
*/
max: number;
}

export type LowerBoundedDomain = DomainMinInterval & LowerBound;
export type UpperBoundedDomain = DomainMinInterval & UpperBound;
export type CompleteBoundedDomain = DomainMinInterval & LowerBound & UpperBound;
export type UnboundedDomainWithInterval = DomainMinInterval;
export type LowerBoundedDomain = DomainBase & LowerBound;
export type UpperBoundedDomain = DomainBase & UpperBound;
export type CompleteBoundedDomain = DomainBase & LowerBound & UpperBound;
export type UnboundedDomainWithInterval = DomainBase;

export type DomainRange = LowerBoundedDomain | UpperBoundedDomain | CompleteBoundedDomain | UnboundedDomainWithInterval;

Expand Down
9 changes: 9 additions & 0 deletions src/utils/data_generators/data_generator.ts
Expand Up @@ -7,6 +7,15 @@ export class DataGenerator {
this.generator = new Simple1DNoise(randomNumberGenerator);
this.frequency = frequency;
}
generateBasicSeries(totalPoints = 50, offset = 0, amplitude = 1) {
const dataPoints = new Array(totalPoints).fill(0).map((_, i) => {
return {
x: i,
y: (this.generator.getValue(i) + offset) * amplitude,
};
});
return dataPoints;
}
generateSimpleSeries(totalPoints = 50, group = 1, groupPrefix = '') {
const dataPoints = new Array(totalPoints).fill(0).map((_, i) => {
return {
Expand Down
62 changes: 50 additions & 12 deletions src/utils/domain.test.ts
Expand Up @@ -118,17 +118,55 @@ describe('utils/domain', () => {
expect(stackedDataDomain).toEqual(expectedStackedDomain);
});

test('should compute domain based on scaleToExtent', () => {
// start & end are positive
expect(computeDomainExtent([5, 10], true)).toEqual([5, 10]);
expect(computeDomainExtent([5, 10], false)).toEqual([0, 10]);

// start & end are negative
expect(computeDomainExtent([-15, -10], true)).toEqual([-15, -10]);
expect(computeDomainExtent([-15, -10], false)).toEqual([-15, 0]);

// start is negative, end is positive
expect(computeDomainExtent([-15, 10], true)).toEqual([-15, 10]);
expect(computeDomainExtent([-15, 10], false)).toEqual([-15, 10]);
describe('scaleToExtent', () => {
describe('true', () => {
it('should find domain when start & end are positive', () => {
expect(computeDomainExtent([5, 10], true)).toEqual([5, 10]);
});
it('should find domain when start & end are negative', () => {
expect(computeDomainExtent([-15, -10], true)).toEqual([-15, -10]);
});
it('should find domain when start is negative, end is positive', () => {
expect(computeDomainExtent([-15, 10], true)).toEqual([-15, 10]);
});
});
describe('false', () => {
it('should find domain when start & end are positive', () => {
expect(computeDomainExtent([5, 10], false)).toEqual([0, 10]);
});
it('should find domain when start & end are negative', () => {
expect(computeDomainExtent([-15, -10], false)).toEqual([-15, 0]);
});
it('should find domain when start is negative, end is positive', () => {
expect(computeDomainExtent([-15, 10], false)).toEqual([-15, 10]);
});
});
});

describe('fitToExtent', () => {
it('should not effect domain when scaleToExtent is true', () => {
expect(computeDomainExtent([5, 10], true)).toEqual([5, 10]);
});

describe('baseline far from zero', () => {
it('should get domain from positive domain', () => {
expect(computeDomainExtent([10, 70], false, true)).toEqual([5, 75]);
});
it('should get domain from positive & negative domain', () => {
expect(computeDomainExtent([-30, 30], false, true)).toEqual([-35, 35]);
});
it('should get domain from negative domain', () => {
expect(computeDomainExtent([-70, -10], false, true)).toEqual([-75, -5]);
});
});

describe('baseline near zero', () => {
it('should set min baseline as 0 if original domain is less than zero', () => {
expect(computeDomainExtent([5, 65], false, true)).toEqual([0, 70]);
});
it('should set max baseline as 0 if original domain is less than zero', () => {
expect(computeDomainExtent([-65, -5], false, true)).toEqual([-70, 0]);
});
});
});
});
31 changes: 26 additions & 5 deletions src/utils/domain.ts
Expand Up @@ -45,17 +45,31 @@ export function computeOrdinalDataDomain(
: uniqueValues;
}

function computeFittedDomain(start?: number, end?: number) {
if (start === undefined || end === undefined) {
return [start, end];
}

const delta = Math.abs(end - start);
const padding = (delta === 0 ? end - 0 : delta) / 12;
const newStart = start - padding;
const newEnd = end + padding;

return [start >= 0 && newStart < 0 ? 0 : newStart, end <= 0 && newEnd > 0 ? 0 : newEnd];
}

export function computeDomainExtent(
computedDomain: [number, number] | [undefined, undefined],
scaleToExtent: boolean,
fitToExtent: boolean = false,
): [number, number] {
const [start, end] = computedDomain;
const [start, end] = fitToExtent && !scaleToExtent ? computeFittedDomain(...computedDomain) : computedDomain;

if (start != null && end != null) {
if (start >= 0 && end >= 0) {
return scaleToExtent ? [start, end] : [0, end];
return scaleToExtent || fitToExtent ? [start, end] : [0, end];
} else if (start < 0 && end < 0) {
return scaleToExtent ? [start, end] : [start, 0];
return scaleToExtent || fitToExtent ? [start, end] : [start, 0];
}
return [start, end];
}
Expand All @@ -64,11 +78,18 @@ export function computeDomainExtent(
return [0, 0];
}

export function computeContinuousDataDomain(data: any[], accessor: AccessorFn, scaleToExtent = false): number[] {
export function computeContinuousDataDomain(
data: any[],
accessor: AccessorFn,
scaleToExtent = false,
fitToExtent = false,
): number[] {
const range = extent(data, accessor);
return computeDomainExtent(range, scaleToExtent);

return computeDomainExtent(range, scaleToExtent, fitToExtent);
}

// TODO: remove or incorporate this function
export function computeStackedContinuousDomain(
data: any[],
xAccessor: AccessorFn,
Expand Down

0 comments on commit bcd08b7

Please sign in to comment.