Skip to content

Commit bcd08b7

Browse files
authored
fix: add domain fitting for tsvb regression (#511)
* 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
1 parent 3301b36 commit bcd08b7

File tree

5 files changed

+118
-32
lines changed

5 files changed

+118
-32
lines changed

src/chart_types/xy_chart/domains/y_domain.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,30 +72,32 @@ function mergeYDomainForGroup(
7272
): YDomain {
7373
const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]);
7474
const { isPercentageStack } = groupSpecs;
75+
const fitToExtent = customDomain && customDomain.fit;
7576

7677
let domain: number[];
7778
if (isPercentageStack) {
78-
domain = computeContinuousDataDomain([0, 1], identity);
79+
domain = computeContinuousDataDomain([0, 1], identity, fitToExtent);
7980
} else {
8081
// compute stacked domain
8182
const isStackedScaleToExtent = groupSpecs.stacked.some((spec) => {
8283
return spec.yScaleToDataExtent;
8384
});
8485
const stackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.stacked);
85-
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent);
86+
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent, fitToExtent);
8687

8788
// compute non stacked domain
8889
const isNonStackedScaleToExtent = groupSpecs.nonStacked.some((spec) => {
8990
return spec.yScaleToDataExtent;
9091
});
9192
const nonStackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.nonStacked);
92-
const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent);
93+
const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent, fitToExtent);
9394

9495
// merge stacked and non stacked domain together
9596
domain = computeContinuousDataDomain(
9697
[...stackedDomain, ...nonStackedDomain],
9798
identity,
9899
isStackedScaleToExtent || isNonStackedScaleToExtent,
100+
fitToExtent,
99101
);
100102

101103
const [computedDomainMin, computedDomainMax] = domain;
@@ -139,7 +141,11 @@ export function getDataSeriesOnGroup(
139141
);
140142
}
141143

142-
function computeYStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boolean): number[] {
144+
function computeYStackedDomain(
145+
dataseries: RawDataSeries[],
146+
scaleToExtent: boolean,
147+
fitToExtent: boolean = false,
148+
): number[] {
143149
const stackMap = new Map<any, any[]>();
144150
dataseries.forEach((ds, index) => {
145151
ds.data.forEach((datum) => {
@@ -158,9 +164,10 @@ function computeYStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boole
158164
if (dataValues.length === 0) {
159165
return [];
160166
}
161-
return computeContinuousDataDomain(dataValues, identity, scaleToExtent);
167+
return computeContinuousDataDomain(dataValues, identity, scaleToExtent, fitToExtent);
162168
}
163-
function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boolean) {
169+
170+
function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boolean, fitToExtent: boolean = false) {
164171
const yValues = new Set<any>();
165172
dataseries.forEach((ds) => {
166173
ds.data.forEach((datum) => {
@@ -173,7 +180,7 @@ function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: bo
173180
if (yValues.size === 0) {
174181
return [];
175182
}
176-
return computeContinuousDataDomain([...yValues.values()], identity, scaleToExtent);
183+
return computeContinuousDataDomain([...yValues.values()], identity, scaleToExtent, fitToExtent);
177184
}
178185
export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) {
179186
const specsByGroupIds = new Map<

src/chart_types/xy_chart/utils/specs.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ export type BarStyleAccessor = (datum: RawDataSeriesDatum, geometryId: GeometryI
4242
export type PointStyleAccessor = (datum: RawDataSeriesDatum, geometryId: GeometryId) => PointStyleOverride;
4343
export const DEFAULT_GLOBAL_ID = '__global__';
4444

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

5764
interface LowerBound {
58-
/** Lower bound of domain range */
65+
/**
66+
* Lower bound of domain range
67+
*/
5968
min: number;
6069
}
6170

6271
interface UpperBound {
63-
/** Upper bound of domain range */
72+
/**
73+
* Upper bound of domain range
74+
*/
6475
max: number;
6576
}
6677

67-
export type LowerBoundedDomain = DomainMinInterval & LowerBound;
68-
export type UpperBoundedDomain = DomainMinInterval & UpperBound;
69-
export type CompleteBoundedDomain = DomainMinInterval & LowerBound & UpperBound;
70-
export type UnboundedDomainWithInterval = DomainMinInterval;
78+
export type LowerBoundedDomain = DomainBase & LowerBound;
79+
export type UpperBoundedDomain = DomainBase & UpperBound;
80+
export type CompleteBoundedDomain = DomainBase & LowerBound & UpperBound;
81+
export type UnboundedDomainWithInterval = DomainBase;
7182

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

src/utils/data_generators/data_generator.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ export class DataGenerator {
77
this.generator = new Simple1DNoise(randomNumberGenerator);
88
this.frequency = frequency;
99
}
10+
generateBasicSeries(totalPoints = 50, offset = 0, amplitude = 1) {
11+
const dataPoints = new Array(totalPoints).fill(0).map((_, i) => {
12+
return {
13+
x: i,
14+
y: (this.generator.getValue(i) + offset) * amplitude,
15+
};
16+
});
17+
return dataPoints;
18+
}
1019
generateSimpleSeries(totalPoints = 50, group = 1, groupPrefix = '') {
1120
const dataPoints = new Array(totalPoints).fill(0).map((_, i) => {
1221
return {

src/utils/domain.test.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,17 +118,55 @@ describe('utils/domain', () => {
118118
expect(stackedDataDomain).toEqual(expectedStackedDomain);
119119
});
120120

121-
test('should compute domain based on scaleToExtent', () => {
122-
// start & end are positive
123-
expect(computeDomainExtent([5, 10], true)).toEqual([5, 10]);
124-
expect(computeDomainExtent([5, 10], false)).toEqual([0, 10]);
125-
126-
// start & end are negative
127-
expect(computeDomainExtent([-15, -10], true)).toEqual([-15, -10]);
128-
expect(computeDomainExtent([-15, -10], false)).toEqual([-15, 0]);
129-
130-
// start is negative, end is positive
131-
expect(computeDomainExtent([-15, 10], true)).toEqual([-15, 10]);
132-
expect(computeDomainExtent([-15, 10], false)).toEqual([-15, 10]);
121+
describe('scaleToExtent', () => {
122+
describe('true', () => {
123+
it('should find domain when start & end are positive', () => {
124+
expect(computeDomainExtent([5, 10], true)).toEqual([5, 10]);
125+
});
126+
it('should find domain when start & end are negative', () => {
127+
expect(computeDomainExtent([-15, -10], true)).toEqual([-15, -10]);
128+
});
129+
it('should find domain when start is negative, end is positive', () => {
130+
expect(computeDomainExtent([-15, 10], true)).toEqual([-15, 10]);
131+
});
132+
});
133+
describe('false', () => {
134+
it('should find domain when start & end are positive', () => {
135+
expect(computeDomainExtent([5, 10], false)).toEqual([0, 10]);
136+
});
137+
it('should find domain when start & end are negative', () => {
138+
expect(computeDomainExtent([-15, -10], false)).toEqual([-15, 0]);
139+
});
140+
it('should find domain when start is negative, end is positive', () => {
141+
expect(computeDomainExtent([-15, 10], false)).toEqual([-15, 10]);
142+
});
143+
});
144+
});
145+
146+
describe('fitToExtent', () => {
147+
it('should not effect domain when scaleToExtent is true', () => {
148+
expect(computeDomainExtent([5, 10], true)).toEqual([5, 10]);
149+
});
150+
151+
describe('baseline far from zero', () => {
152+
it('should get domain from positive domain', () => {
153+
expect(computeDomainExtent([10, 70], false, true)).toEqual([5, 75]);
154+
});
155+
it('should get domain from positive & negative domain', () => {
156+
expect(computeDomainExtent([-30, 30], false, true)).toEqual([-35, 35]);
157+
});
158+
it('should get domain from negative domain', () => {
159+
expect(computeDomainExtent([-70, -10], false, true)).toEqual([-75, -5]);
160+
});
161+
});
162+
163+
describe('baseline near zero', () => {
164+
it('should set min baseline as 0 if original domain is less than zero', () => {
165+
expect(computeDomainExtent([5, 65], false, true)).toEqual([0, 70]);
166+
});
167+
it('should set max baseline as 0 if original domain is less than zero', () => {
168+
expect(computeDomainExtent([-65, -5], false, true)).toEqual([-70, 0]);
169+
});
170+
});
133171
});
134172
});

src/utils/domain.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,31 @@ export function computeOrdinalDataDomain(
4545
: uniqueValues;
4646
}
4747

48+
function computeFittedDomain(start?: number, end?: number) {
49+
if (start === undefined || end === undefined) {
50+
return [start, end];
51+
}
52+
53+
const delta = Math.abs(end - start);
54+
const padding = (delta === 0 ? end - 0 : delta) / 12;
55+
const newStart = start - padding;
56+
const newEnd = end + padding;
57+
58+
return [start >= 0 && newStart < 0 ? 0 : newStart, end <= 0 && newEnd > 0 ? 0 : newEnd];
59+
}
60+
4861
export function computeDomainExtent(
4962
computedDomain: [number, number] | [undefined, undefined],
5063
scaleToExtent: boolean,
64+
fitToExtent: boolean = false,
5165
): [number, number] {
52-
const [start, end] = computedDomain;
66+
const [start, end] = fitToExtent && !scaleToExtent ? computeFittedDomain(...computedDomain) : computedDomain;
5367

5468
if (start != null && end != null) {
5569
if (start >= 0 && end >= 0) {
56-
return scaleToExtent ? [start, end] : [0, end];
70+
return scaleToExtent || fitToExtent ? [start, end] : [0, end];
5771
} else if (start < 0 && end < 0) {
58-
return scaleToExtent ? [start, end] : [start, 0];
72+
return scaleToExtent || fitToExtent ? [start, end] : [start, 0];
5973
}
6074
return [start, end];
6175
}
@@ -64,11 +78,18 @@ export function computeDomainExtent(
6478
return [0, 0];
6579
}
6680

67-
export function computeContinuousDataDomain(data: any[], accessor: AccessorFn, scaleToExtent = false): number[] {
81+
export function computeContinuousDataDomain(
82+
data: any[],
83+
accessor: AccessorFn,
84+
scaleToExtent = false,
85+
fitToExtent = false,
86+
): number[] {
6887
const range = extent(data, accessor);
69-
return computeDomainExtent(range, scaleToExtent);
88+
89+
return computeDomainExtent(range, scaleToExtent, fitToExtent);
7090
}
7191

92+
// TODO: remove or incorporate this function
7293
export function computeStackedContinuousDomain(
7394
data: any[],
7495
xAccessor: AccessorFn,

0 commit comments

Comments
 (0)