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

feat: allow partial custom domain #116

Merged
merged 8 commits into from
Mar 25, 2019
142 changes: 142 additions & 0 deletions src/lib/axes/axis_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getVerticalAxisGridLineProps,
getVerticalAxisTickLineProps,
getVisibleTicks,
isBounded,
isHorizontal,
isVertical,
isYDomain,
Expand Down Expand Up @@ -1034,6 +1035,134 @@ describe('Axis computational utils', () => {
);
});

test('should merge axis domains by group id: partial upper bounded prevDomain with complete domain', () => {
const groupId = getGroupId('group_1');
const domainRange1 = {
max: 9,
};

const domainRange2 = {
min: 0,
max: 7,
};

verticalAxisSpec.domain = domainRange1;

const axesSpecs = new Map();
axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec);

const axis2 = { ...verticalAxisSpec, id: getAxisId('axis2') };

axis2.domain = domainRange2;
axesSpecs.set(axis2.id, axis2);

const expectedMergedMap = new Map<GroupId, DomainRange>();
expectedMergedMap.set(groupId, { min: 0, max: 9 });

const mergedDomainsByGroupId = mergeDomainsByGroupId(axesSpecs, 0);
expect(mergedDomainsByGroupId).toEqual(expectedMergedMap);
});

test('should merge axis domains by group id: partial lower bounded prevDomain with complete domain', () => {
const groupId = getGroupId('group_1');
const domainRange1 = {
min: -1,
};

const domainRange2 = {
min: 0,
max: 7,
};

verticalAxisSpec.domain = domainRange1;

const axesSpecs = new Map();
axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec);

const axis2 = { ...verticalAxisSpec, id: getAxisId('axis2') };

axis2.domain = domainRange2;
axesSpecs.set(axis2.id, axis2);

const expectedMergedMap = new Map<GroupId, DomainRange>();
expectedMergedMap.set(groupId, { min: -1, max: 7 });

const mergedDomainsByGroupId = mergeDomainsByGroupId(axesSpecs, 0);
expect(mergedDomainsByGroupId).toEqual(expectedMergedMap);
});

test('should merge axis domains by group id: partial upper bounded prevDomain with lower bounded domain', () => {
const groupId = getGroupId('group_1');
const domainRange1 = {
max: 9,
};

const domainRange2 = {
min: 0,
};

const domainRange3 = {
min: -1,
};

verticalAxisSpec.domain = domainRange1;

const axesSpecs = new Map();
axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec);

const axis2 = { ...verticalAxisSpec, id: getAxisId('axis2') };

axis2.domain = domainRange2;
axesSpecs.set(axis2.id, axis2);

const axis3 = { ...verticalAxisSpec, id: getAxisId('axis3') };

axis3.domain = domainRange3;
axesSpecs.set(axis3.id, axis3);

const expectedMergedMap = new Map<GroupId, DomainRange>();
expectedMergedMap.set(groupId, { min: -1, max: 9 });

const mergedDomainsByGroupId = mergeDomainsByGroupId(axesSpecs, 0);
expect(mergedDomainsByGroupId).toEqual(expectedMergedMap);
});

test('should merge axis domains by group id: partial lower bounded prevDomain with upper bounded domain', () => {
const groupId = getGroupId('group_1');
const domainRange1 = {
min: 2,
};

const domainRange2 = {
max: 7,
};

const domainRange3 = {
max: 9,
};

verticalAxisSpec.domain = domainRange1;

const axesSpecs = new Map();
axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec);

const axis2 = { ...verticalAxisSpec, id: getAxisId('axis2') };

axis2.domain = domainRange2;
axesSpecs.set(axis2.id, axis2);

const axis3 = { ...verticalAxisSpec, id: getAxisId('axis3') };

axis3.domain = domainRange3;
axesSpecs.set(axis3.id, axis3);

const expectedMergedMap = new Map<GroupId, DomainRange>();
expectedMergedMap.set(groupId, { min: 2, max: 9 });

const mergedDomainsByGroupId = mergeDomainsByGroupId(axesSpecs, 0);
expect(mergedDomainsByGroupId).toEqual(expectedMergedMap);
});

test('should throw on invalid domain', () => {
const domainRange1 = {
min: 9,
Expand All @@ -1052,4 +1181,17 @@ describe('Axis computational utils', () => {

expect(attemptToMerge).toThrowError(expectedError);
});

test('should determine that a domain has at least one bound', () => {
const lowerBounded = {
min: 0,
};

const upperBounded = {
max: 0,
};

expect(isBounded(lowerBounded)).toBe(true);
expect(isBounded(upperBounded)).toBe(true);
});
});
54 changes: 49 additions & 5 deletions src/lib/axes/axis_utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { XDomain } from '../series/domains/x_domain';
import { YDomain } from '../series/domains/y_domain';
import { computeXScale, computeYScales } from '../series/scales';
import { AxisSpec, DomainRange, Position, Rotation, TickFormatter } from '../series/specs';
import {
AxisSpec,
CompleteBoundedDomain,
DomainRange,
LowerBoundedDomain,
Position,
Rotation,
TickFormatter,
UpperBoundedDomain,
} from '../series/specs';
import { AxisConfig, Theme } from '../themes/theme';
import { Dimensions, Margins } from '../utils/dimensions';
import { Domain } from '../utils/domain';
Expand Down Expand Up @@ -620,6 +629,22 @@ export function isHorizontal(position: Position) {
return !isVertical(position);
}

export function isLowerBound(domain: Partial<CompleteBoundedDomain>): domain is LowerBoundedDomain {
return domain.min != null;
}

export function isUpperBound(domain: Partial<CompleteBoundedDomain>): domain is UpperBoundedDomain {
return domain.max != null;
}

export function isCompleteBound(domain: Partial<CompleteBoundedDomain>): domain is CompleteBoundedDomain {
return domain.max != null && domain.min != null;
}

export function isBounded(domain: Partial<CompleteBoundedDomain>): domain is DomainRange {
return domain.max != null || domain.min != null;
}

export function mergeDomainsByGroupId(
axesSpecs: Map<AxisId, AxisSpec>,
chartRotation: Rotation,
Expand All @@ -640,20 +665,39 @@ export function mergeDomainsByGroupId(
throw new Error(errorMessage);
}

if (domain.min > domain.max) {
if (isCompleteBound(domain) && domain.min > domain.max) {
const errorMessage = `[Axis ${id}]: custom domain is invalid, min is greater than max`;
throw new Error(errorMessage);
}

const prevGroupDomain = domainsByGroupId.get(groupId);

if (prevGroupDomain) {
const prevDomain = prevGroupDomain as DomainRange;

const prevMin = (isLowerBound(prevDomain)) ? prevDomain.min : undefined;
const prevMax = (isUpperBound(prevDomain)) ? prevDomain.max : undefined;

let max = prevMax;
let min = prevMin;

if (isCompleteBound(domain)) {
min = prevMin != null ? Math.min(domain.min, prevMin) : domain.min;
max = prevMax != null ? Math.max(domain.max, prevMax) : domain.max;
} else if (isLowerBound(domain)) {
min = prevMin != null ? Math.min(domain.min, prevMin) : domain.min;
} else if (isUpperBound(domain)) {
max = prevMax != null ? Math.max(domain.max, prevMax) : domain.max;
}

const mergedDomain = {
min: Math.min(domain.min, prevGroupDomain.min),
max: Math.max(domain.max, prevGroupDomain.max),
min,
max,
};

domainsByGroupId.set(groupId, mergedDomain);
if (isBounded(mergedDomain)) {
domainsByGroupId.set(groupId, mergedDomain);
}
} else {
domainsByGroupId.set(groupId, domain);
}
Expand Down
36 changes: 35 additions & 1 deletion src/lib/series/domains/x_domain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ describe('X Domain', () => {
const minInterval = findMinInterval([]);
expect(minInterval).toBe(0);
});
test('should account for custom domain when merging a linear domain', () => {
test('should account for custom domain when merging a linear domain: complete bounded domain', () => {
const xValues = new Set([1, 2, 3, 4, 5]);
const xDomain = { min: 0, max: 3 };
const specs: Array<Pick<BasicSeriesSpec, 'seriesType' | 'xScaleType'>> = [
Expand All @@ -659,6 +659,40 @@ describe('X Domain', () => {
expect(attemptToMerge).toThrowError('custom xDomain is invalid, min is greater than max');
});

test('should account for custom domain when merging a linear domain: lower bounded domain', () => {
const xValues = new Set([1, 2, 3, 4, 5]);
const xDomain = { min: 0 };
const specs: Array<Pick<BasicSeriesSpec, 'seriesType' | 'xScaleType'>> = [
{ seriesType: 'line', xScaleType: ScaleType.Linear },
];

const mergedDomain = mergeXDomain(specs, xValues, xDomain);
expect(mergedDomain.domain).toEqual([0, 5]);

const invalidXDomain = { min: 10 };
const attemptToMerge = () => {
mergeXDomain(specs, xValues, invalidXDomain);
};
expect(attemptToMerge).toThrowError('custom xDomain is invalid, custom min is greater than computed max');
});

test('should account for custom domain when merging a linear domain: upper bounded domain', () => {
const xValues = new Set([1, 2, 3, 4, 5]);
const xDomain = { max: 3 };
const specs: Array<Pick<BasicSeriesSpec, 'seriesType' | 'xScaleType'>> = [
{ seriesType: 'line', xScaleType: ScaleType.Linear },
];

const mergedDomain = mergeXDomain(specs, xValues, xDomain);
expect(mergedDomain.domain).toEqual([1, 3]);

const invalidXDomain = { max: -1 };
const attemptToMerge = () => {
mergeXDomain(specs, xValues, invalidXDomain);
};
expect(attemptToMerge).toThrowError('custom xDomain is invalid, computed min is greater than custom max');
});

test('should account for custom domain when merging an ordinal domain', () => {
const xValues = new Set(['a', 'b', 'c', 'd']);
const xDomain = ['a', 'b', 'c'];
Expand Down
24 changes: 21 additions & 3 deletions src/lib/series/domains/x_domain.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isCompleteBound, isLowerBound, isUpperBound } from '../../axes/axis_utils';
import { compareByValueAsc, identity } from '../../utils/commons';
import { computeContinuousDataDomain, computeOrdinalDataDomain, Domain } from '../../utils/domain';
import { ScaleType } from '../../utils/scales/scales';
Expand Down Expand Up @@ -42,10 +43,27 @@ export function mergeXDomain(
seriesXComputedDomains = computeContinuousDataDomain(values, identity, true);
if (xDomain) {
if (!Array.isArray(xDomain)) {
if (xDomain.min > xDomain.max) {
throw new Error('custom xDomain is invalid, min is greater than max');
const [computedDomainMin, computedDomainMax] = seriesXComputedDomains;

if (isCompleteBound(xDomain)) {
if (xDomain.min > xDomain.max) {
throw new Error('custom xDomain is invalid, min is greater than max');
}

seriesXComputedDomains = [xDomain.min, xDomain.max];
} else if (isLowerBound(xDomain)) {
if (xDomain.min > computedDomainMax) {
throw new Error('custom xDomain is invalid, custom min is greater than computed max');
}

seriesXComputedDomains = [xDomain.min, computedDomainMax];
} else if (isUpperBound(xDomain)) {
if (computedDomainMin > xDomain.max) {
throw new Error('custom xDomain is invalid, computed min is greater than custom max');
}

seriesXComputedDomains = [computedDomainMin, xDomain.max];
}
seriesXComputedDomains = [xDomain.min, xDomain.max];
} else {
throw new Error('xDomain for continuous scale should be a DomainRange object, not an array');
}
Expand Down
Loading