Skip to content

Commit

Permalink
feat: allow partial custom domain (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmacunningham authored Mar 25, 2019
1 parent 78625cc commit d0b6b19
Show file tree
Hide file tree
Showing 8 changed files with 493 additions and 21 deletions.
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

0 comments on commit d0b6b19

Please sign in to comment.