Skip to content

Commit 4b56729

Browse files
committed
feat: add option to fit y domain to data
1 parent 063535e commit 4b56729

6 files changed

Lines changed: 178 additions & 43 deletions

File tree

src/chart_types/xy_chart/domains/y_domain.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,27 +75,32 @@ function mergeYDomainForGroup(
7575

7676
let domain: number[];
7777
if (isPercentageStack) {
78-
domain = computeContinuousDataDomain([0, 1], identity);
78+
domain = computeContinuousDataDomain([0, 1], identity, customDomain?.fit);
7979
} else {
8080
// compute stacked domain
8181
const isStackedScaleToExtent = groupSpecs.stacked.some((spec) => {
8282
return spec.yScaleToDataExtent;
8383
});
8484
const stackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.stacked);
85-
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent);
85+
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent, customDomain?.fit);
8686

8787
// compute non stacked domain
8888
const isNonStackedScaleToExtent = groupSpecs.nonStacked.some((spec) => {
8989
return spec.yScaleToDataExtent;
9090
});
9191
const nonStackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.nonStacked);
92-
const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent);
92+
const nonStackedDomain = computeYNonStackedDomain(
93+
nonStackedDataSeries,
94+
isNonStackedScaleToExtent,
95+
customDomain?.fit,
96+
);
9397

9498
// merge stacked and non stacked domain together
9599
domain = computeContinuousDataDomain(
96100
[...stackedDomain, ...nonStackedDomain],
97101
identity,
98102
isStackedScaleToExtent || isNonStackedScaleToExtent,
103+
customDomain?.fit,
99104
);
100105

101106
const [computedDomainMin, computedDomainMax] = domain;
@@ -136,7 +141,11 @@ export function getDataSeriesOnGroup(
136141
}, [] as RawDataSeries[]);
137142
}
138143

139-
function computeYStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boolean): number[] {
144+
function computeYStackedDomain(
145+
dataseries: RawDataSeries[],
146+
scaleToExtent: boolean,
147+
fitToExtent: boolean = false,
148+
): number[] {
140149
const stackMap = new Map<any, any[]>();
141150
dataseries.forEach((ds, index) => {
142151
ds.data.forEach((datum) => {
@@ -155,10 +164,10 @@ function computeYStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boole
155164
if (dataValues.length === 0) {
156165
return [];
157166
}
158-
return computeContinuousDataDomain(dataValues, identity, scaleToExtent);
167+
return computeContinuousDataDomain(dataValues, identity, scaleToExtent, fitToExtent);
159168
}
160169

161-
function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boolean) {
170+
function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: boolean, fitToExtent: boolean = false) {
162171
const yValues = new Set<any>();
163172
dataseries.forEach((ds) => {
164173
ds.data.forEach((datum) => {
@@ -171,7 +180,7 @@ function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: bo
171180
if (yValues.size === 0) {
172181
return [];
173182
}
174-
return computeContinuousDataDomain([...yValues.values()], identity, scaleToExtent);
183+
return computeContinuousDataDomain([...yValues.values()], identity, scaleToExtent, fitToExtent);
175184
}
176185

177186
export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) {

src/chart_types/xy_chart/utils/specs.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,6 @@ export type SubSeriesStringPredicate = (
7070
isTooltip: boolean,
7171
) => string | number | null;
7272

73-
interface DomainMinInterval {
74-
/** Custom minInterval for the domain which will affect data bucket size.
75-
* The minInterval cannot be greater than the computed minimum interval between any two adjacent data points.
76-
* Further, if you specify a custom numeric minInterval for a timeseries, please note that due to the restriction
77-
* above, the specified numeric minInterval will be interpreted as a fixed interval.
78-
* This means that, for example, if you have yearly timeseries data that ranges from 2016 to 2019 and you manually
79-
* compute the interval between 2016 and 2017, you'll have 366 days due to 2016 being a leap year. This will not
80-
* be a valid interval because it is greater than the computed minInterval of 365 days betwen the other years.
81-
*/
82-
minInterval?: number;
83-
}
84-
8573
/**
8674
* The fit function type
8775
*/
@@ -166,20 +154,43 @@ export const Fit = Object.freeze({
166154

167155
export type Fit = $Values<typeof Fit>;
168156

157+
interface DomainBase {
158+
/**
159+
* Custom minInterval for the domain which will affect data bucket size.
160+
* The minInterval cannot be greater than the computed minimum interval between any two adjacent data points.
161+
* Further, if you specify a custom numeric minInterval for a timeseries, please note that due to the restriction
162+
* above, the specified numeric minInterval will be interpreted as a fixed interval.
163+
* This means that, for example, if you have yearly timeseries data that ranges from 2016 to 2019 and you manually
164+
* compute the interval between 2016 and 2017, you'll have 366 days due to 2016 being a leap year. This will not
165+
* be a valid interval because it is greater than the computed minInterval of 365 days betwen the other years.
166+
*/
167+
minInterval?: number;
168+
/**
169+
* Whether to fit the domain to the data. ONLY applies to `yDomains`
170+
*
171+
* Setting `max` or `min` will override this functionality.
172+
*/
173+
fit?: boolean;
174+
}
175+
169176
interface LowerBound {
170-
/** Lower bound of domain range */
177+
/**
178+
* Lower bound of domain range
179+
*/
171180
min: number;
172181
}
173182

174183
interface UpperBound {
175-
/** Upper bound of domain range */
184+
/**
185+
* Upper bound of domain range
186+
*/
176187
max: number;
177188
}
178189

179-
export type LowerBoundedDomain = DomainMinInterval & LowerBound;
180-
export type UpperBoundedDomain = DomainMinInterval & UpperBound;
181-
export type CompleteBoundedDomain = DomainMinInterval & LowerBound & UpperBound;
182-
export type UnboundedDomainWithInterval = DomainMinInterval;
190+
export type LowerBoundedDomain = DomainBase & LowerBound;
191+
export type UpperBoundedDomain = DomainBase & UpperBound;
192+
export type CompleteBoundedDomain = DomainBase & LowerBound & UpperBound;
193+
export type UnboundedDomainWithInterval = DomainBase;
183194

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

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, groupIndex = 1, groupPrefix = '') {
1120
const group = String.fromCharCode(97 + groupIndex);
1221
const dataPoints = new Array(totalPoints).fill(0).map((_, i) => {

src/utils/domain.test.ts

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

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

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,

stories/axis.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { boolean, number } from '@storybook/addon-knobs';
1+
import { boolean, number, select } from '@storybook/addon-knobs';
22
import { storiesOf } from '@storybook/react';
33
import React from 'react';
44

@@ -614,4 +614,51 @@ storiesOf('Axis', module)
614614
/>
615615
</Chart>
616616
);
617+
})
618+
.add('fit domain to extent in y axis', () => {
619+
const dg = new SeededDataGenerator();
620+
const base = dg.generateBasicSeries(100, 0, 50);
621+
const positive = base.map(({ x, y }) => ({ x, y: y + 1000 }));
622+
const both = base.map(({ x, y }) => ({ x, y: y - 100 }));
623+
const negative = base.map(({ x, y }) => ({ x, y: y - 1000 }));
624+
625+
const dataTypes = {
626+
positive,
627+
both,
628+
negative,
629+
};
630+
const dataKey = select<string>(
631+
'dataset',
632+
{
633+
'Positive values only': 'positive',
634+
'Positive and negative': 'both',
635+
'Negtive values only': 'negative',
636+
},
637+
'both',
638+
);
639+
// @ts-ignore
640+
const dataset = dataTypes[dataKey];
641+
const fit = boolean('fit domain to data', true);
642+
643+
return (
644+
<Chart className={'story-chart'}>
645+
<Axis id={getAxisId('bottom')} title={'index'} position={Position.Bottom} />
646+
<Axis
647+
domain={{ fit }}
648+
id={getAxisId('left')}
649+
title="Value"
650+
position={Position.Left}
651+
tickFormat={(d) => Number(d).toFixed(2)}
652+
/>
653+
654+
<LineSeries
655+
id={getSpecId('lines')}
656+
xScaleType={ScaleType.Linear}
657+
yScaleType={ScaleType.Linear}
658+
xAccessor="x"
659+
yAccessors={['y']}
660+
data={dataset}
661+
/>
662+
</Chart>
663+
);
617664
});

0 commit comments

Comments
 (0)