mirror of
https://github.com/apache/superset.git
synced 2024-09-19 20:19:37 -04:00
feat(encodable): make applyDomain() able to handle domain from dataset (#254)
* feat: make applyDomain() able to handle domain from dataset * test: add unit tests * fix: rename variable
This commit is contained in:
parent
497a4b0ea6
commit
f5f944b405
@ -0,0 +1,29 @@
|
|||||||
|
import { ChannelInput } from '../../types/Channel';
|
||||||
|
import { ScaleType } from '../../types/VegaLite';
|
||||||
|
import { timeScaleTypesSet } from '../scale/scaleCategories';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert each element in the array into
|
||||||
|
* - Date (for time scales)
|
||||||
|
* - number (for other continuous scales)
|
||||||
|
* @param domain
|
||||||
|
* @param scaleType
|
||||||
|
*/
|
||||||
|
export default function parseContinuousDomain<T extends ChannelInput>(
|
||||||
|
domain: T[],
|
||||||
|
scaleType: ScaleType,
|
||||||
|
) {
|
||||||
|
if (timeScaleTypesSet.has(scaleType)) {
|
||||||
|
type TimeDomain = Exclude<T, string | number | boolean>[];
|
||||||
|
|
||||||
|
return domain
|
||||||
|
.filter(d => typeof d !== 'boolean')
|
||||||
|
.map(d => (typeof d === 'string' || typeof d === 'number' ? new Date(d) : d)) as TimeDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NumberDomain = Exclude<T, string | boolean>[];
|
||||||
|
|
||||||
|
return domain.map(d =>
|
||||||
|
typeof d === 'string' || typeof d === 'boolean' ? Number(d) : d,
|
||||||
|
) as NumberDomain;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { ChannelInput } from '../../types/Channel';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discrete domains are converted into string[]
|
||||||
|
* when using D3 scales
|
||||||
|
* @param domain
|
||||||
|
*/
|
||||||
|
export default function parseDiscreteDomain<T extends ChannelInput>(domain: T[]) {
|
||||||
|
return domain.map(d => `${d}`);
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { isDateTime } from 'vega-lite/build/src/datetime';
|
||||||
|
import { DateTime } from '../types/VegaLite';
|
||||||
|
import parseDateTime from './parseDateTime';
|
||||||
|
|
||||||
|
export default function parseDateTimeIfPossible<T>(d: DateTime | T) {
|
||||||
|
return !(d instanceof Date) && isDateTime(d) ? parseDateTime(d) : d;
|
||||||
|
}
|
@ -1,25 +1,57 @@
|
|||||||
import { isDateTime } from 'vega-lite/build/src/datetime';
|
|
||||||
import { Value } from '../../types/VegaLite';
|
import { Value } from '../../types/VegaLite';
|
||||||
import { ScaleConfig, D3Scale } from '../../types/Scale';
|
import { ScaleConfig, D3Scale } from '../../types/Scale';
|
||||||
import parseDateTime from '../parseDateTime';
|
|
||||||
import inferElementTypeFromUnionOfArrayTypes from '../../utils/inferElementTypeFromUnionOfArrayTypes';
|
import inferElementTypeFromUnionOfArrayTypes from '../../utils/inferElementTypeFromUnionOfArrayTypes';
|
||||||
import { isEveryElementDefined } from '../../typeGuards/Base';
|
import { isContinuousScale } from '../../typeGuards/Scale';
|
||||||
|
import combineCategories from '../../utils/combineCategories';
|
||||||
|
import parseDateTimeIfPossible from '../parseDateTimeIfPossible';
|
||||||
|
import parseContinuousDomain from '../domain/parseContinuousDomain';
|
||||||
|
import parseDiscreteDomain from '../domain/parseDiscreteDomain';
|
||||||
|
import combineContinuousDomains from '../../utils/combineContinuousDomains';
|
||||||
|
|
||||||
|
function createOrderFunction(reverse: boolean | undefined) {
|
||||||
|
return reverse ? <T>(array: T[]) => array.slice().reverse() : <T>(array: T[]) => array;
|
||||||
|
}
|
||||||
|
|
||||||
export default function applyDomain<Output extends Value>(
|
export default function applyDomain<Output extends Value>(
|
||||||
config: ScaleConfig<Output>,
|
config: ScaleConfig<Output>,
|
||||||
scale: D3Scale<Output>,
|
scale: D3Scale<Output>,
|
||||||
|
domainFromDataset?: string[] | number[] | boolean[] | Date[],
|
||||||
) {
|
) {
|
||||||
const { domain, reverse } = config;
|
const { domain, reverse, type } = config;
|
||||||
if (typeof domain !== 'undefined' && domain.length > 0) {
|
|
||||||
const processedDomain = inferElementTypeFromUnionOfArrayTypes(domain);
|
|
||||||
|
|
||||||
// Only set domain if all items are defined
|
const order = createOrderFunction(reverse);
|
||||||
if (isEveryElementDefined(processedDomain)) {
|
|
||||||
|
const inputDomain =
|
||||||
|
domainFromDataset && domainFromDataset.length
|
||||||
|
? inferElementTypeFromUnionOfArrayTypes(domainFromDataset)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (domain && domain.length) {
|
||||||
|
const fixedDomain = inferElementTypeFromUnionOfArrayTypes(domain).map(parseDateTimeIfPossible);
|
||||||
|
|
||||||
|
if (isContinuousScale(scale, type)) {
|
||||||
|
const combined = combineContinuousDomains(
|
||||||
|
parseContinuousDomain(fixedDomain, type),
|
||||||
|
inputDomain && parseContinuousDomain(inputDomain, type),
|
||||||
|
);
|
||||||
|
if (combined) {
|
||||||
|
scale.domain(order(combined));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
scale.domain(
|
scale.domain(
|
||||||
(reverse ? processedDomain.slice().reverse() : processedDomain).map(d =>
|
order(
|
||||||
isDateTime(d) ? parseDateTime(d) : d,
|
combineCategories(
|
||||||
|
parseDiscreteDomain(fixedDomain),
|
||||||
|
inputDomain && parseDiscreteDomain(inputDomain),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (inputDomain) {
|
||||||
|
if (isContinuousScale(scale, type)) {
|
||||||
|
scale.domain(order(parseContinuousDomain(inputDomain, type)));
|
||||||
|
} else {
|
||||||
|
scale.domain(order(parseDiscreteDomain(inputDomain)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ export function isNotArray<T>(maybeArray: T | T[]): maybeArray is T {
|
|||||||
return !Array.isArray(maybeArray);
|
return !Array.isArray(maybeArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDefined<T>(value: any): value is T {
|
export function isDefined<T>(value: T | undefined | null): value is T {
|
||||||
return typeof value !== 'undefined' && value !== null;
|
return typeof value !== 'undefined' && value !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,13 @@ export interface CombinedScaleConfig<Output extends Value = Value>
|
|||||||
/**
|
/**
|
||||||
* domain of the scale
|
* domain of the scale
|
||||||
*/
|
*/
|
||||||
domain?: (number | undefined | null)[] | string[] | boolean[] | (DateTime | undefined | null)[];
|
domain?:
|
||||||
|
| number[]
|
||||||
|
| string[]
|
||||||
|
| boolean[]
|
||||||
|
| DateTime[]
|
||||||
|
| (number | undefined | null)[]
|
||||||
|
| (DateTime | undefined | null)[];
|
||||||
/**
|
/**
|
||||||
* range of the scale
|
* range of the scale
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Combine two arrays into a unique list
|
||||||
|
* by keeping the order the fixedCategories
|
||||||
|
* and append new categories at the end.
|
||||||
|
* @param fixedCategories
|
||||||
|
* @param inputCategories
|
||||||
|
*/
|
||||||
|
export default function combineCategories<T>(fixedCategories: T[], inputCategories: T[] = []) {
|
||||||
|
if (fixedCategories.length === 0) {
|
||||||
|
return inputCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixedSet = new Set(fixedCategories);
|
||||||
|
|
||||||
|
return fixedCategories.concat(inputCategories.filter(d => !fixedSet.has(d)));
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import { isEveryElementDefined, isDefined } from '../typeGuards/Base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine two continuous domain and ensure that the output
|
||||||
|
* does not go beyond fixedDomain
|
||||||
|
* @param userSpecifiedDomain
|
||||||
|
* @param dataDomain
|
||||||
|
*/
|
||||||
|
export default function combineContinuousDomains(
|
||||||
|
userSpecifiedDomain: (number | Date | null | undefined)[],
|
||||||
|
dataDomain?: (number | Date)[],
|
||||||
|
) {
|
||||||
|
if (userSpecifiedDomain.length > 0 && isEveryElementDefined(userSpecifiedDomain)) {
|
||||||
|
return userSpecifiedDomain;
|
||||||
|
} else if (dataDomain) {
|
||||||
|
if (
|
||||||
|
userSpecifiedDomain.length === 2 &&
|
||||||
|
dataDomain.length === 2 &&
|
||||||
|
userSpecifiedDomain.filter(isDefined).length > 0
|
||||||
|
) {
|
||||||
|
const [userSpecifiedMin, userSpecifiedMax] = userSpecifiedDomain;
|
||||||
|
const [dataMin, dataMax] = dataDomain;
|
||||||
|
let min = dataMin;
|
||||||
|
if (isDefined(userSpecifiedMin)) {
|
||||||
|
min = userSpecifiedMin.valueOf() > dataMin.valueOf() ? userSpecifiedMin : dataMin;
|
||||||
|
}
|
||||||
|
let max = dataMax;
|
||||||
|
if (isDefined(userSpecifiedMax)) {
|
||||||
|
max = userSpecifiedMax.valueOf() < dataMax.valueOf() ? userSpecifiedMax : dataMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [min, max];
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import parseContinuousDomain from '../../../src/parsers/domain/parseContinuousDomain';
|
||||||
|
|
||||||
|
describe('parseContinuousDomain()', () => {
|
||||||
|
it('parses time for time scale', () => {
|
||||||
|
expect(
|
||||||
|
parseContinuousDomain(
|
||||||
|
[0, 1, true, false, '2019-01-01', new Date(2019, 10, 1), null, undefined],
|
||||||
|
'time',
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
new Date(0),
|
||||||
|
new Date(1),
|
||||||
|
new Date('2019-01-01'),
|
||||||
|
new Date(2019, 10, 1),
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('parses number or leave date as-is for other scale', () => {
|
||||||
|
expect(
|
||||||
|
parseContinuousDomain(
|
||||||
|
[0, 1, true, false, '2019-01-01', new Date(2019, 10, 1), null, undefined],
|
||||||
|
'linear',
|
||||||
|
),
|
||||||
|
).toEqual([0, 1, 1, 0, NaN, new Date(2019, 10, 1), null, undefined]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,9 @@
|
|||||||
|
import parseDiscreteDomain from '../../../src/parsers/domain/parseDiscreteDomain';
|
||||||
|
|
||||||
|
describe('parseDiscreteDomain()', () => {
|
||||||
|
it('parses every element to string', () => {
|
||||||
|
expect(
|
||||||
|
parseDiscreteDomain([1560384000000, 'abc', false, true, 0, 1, null, undefined]),
|
||||||
|
).toEqual(['1560384000000', 'abc', 'false', 'true', '0', '1', 'null', 'undefined']);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,56 @@
|
|||||||
|
import { scaleLinear, scaleOrdinal } from 'd3-scale';
|
||||||
|
import applyDomain from '../../../src/parsers/scale/applyDomain';
|
||||||
|
import { HasToString } from '../../../src/types/Base';
|
||||||
|
|
||||||
|
describe('applyDomain()', () => {
|
||||||
|
describe('with scale.domain', () => {
|
||||||
|
describe('with domainFromDataset', () => {
|
||||||
|
it('continuous domain', () => {
|
||||||
|
const scale = scaleLinear();
|
||||||
|
applyDomain({ type: 'linear', domain: [null, 10] }, scale, [1, 20]);
|
||||||
|
expect(scale.domain()).toEqual([1, 10]);
|
||||||
|
});
|
||||||
|
it('discrete domain', () => {
|
||||||
|
const scale = scaleOrdinal<HasToString, string>();
|
||||||
|
applyDomain(
|
||||||
|
{ type: 'ordinal', domain: ['a', 'c'], range: ['red', 'green', 'blue'] },
|
||||||
|
scale,
|
||||||
|
['a', 'b', 'c'],
|
||||||
|
);
|
||||||
|
expect(scale.domain()).toEqual(['a', 'c', 'b']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('without domainFromDataset', () => {
|
||||||
|
it('continuous domain', () => {
|
||||||
|
const scale = scaleLinear();
|
||||||
|
applyDomain({ type: 'linear', domain: [1, 10] }, scale);
|
||||||
|
expect(scale.domain()).toEqual([1, 10]);
|
||||||
|
});
|
||||||
|
it('discrete domain', () => {
|
||||||
|
const scale = scaleOrdinal<HasToString, string>();
|
||||||
|
applyDomain(
|
||||||
|
{ type: 'ordinal', domain: ['a', 'c'], range: ['red', 'green', 'blue'] },
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
expect(scale.domain()).toEqual(['a', 'c']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('with domainFromDataset only', () => {
|
||||||
|
it('continuous domain', () => {
|
||||||
|
const scale = scaleLinear();
|
||||||
|
applyDomain({ type: 'linear' }, scale, [1, 20]);
|
||||||
|
expect(scale.domain()).toEqual([1, 20]);
|
||||||
|
});
|
||||||
|
it('discrete domain', () => {
|
||||||
|
const scale = scaleOrdinal<HasToString, string>();
|
||||||
|
applyDomain({ type: 'ordinal', range: ['red', 'green', 'blue'] }, scale, ['a', 'b', 'c']);
|
||||||
|
expect(scale.domain()).toEqual(['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('uses default domain if none is specified', () => {
|
||||||
|
const scale = scaleLinear();
|
||||||
|
applyDomain({ type: 'linear' }, scale);
|
||||||
|
expect(scale.domain()).toEqual([0, 1]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
import combineCategories from '../../src/utils/combineCategories';
|
||||||
|
|
||||||
|
describe('combineCategories()', () => {
|
||||||
|
it('adds all categories from the first list and add new categories from the second list at the end', () => {
|
||||||
|
expect(combineCategories(['fish', 'beef', 'lamb'], ['lamb', 'fish', 'pork'])).toEqual([
|
||||||
|
'fish',
|
||||||
|
'beef',
|
||||||
|
'lamb',
|
||||||
|
'pork',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('works if the first list is empty', () => {
|
||||||
|
expect(combineCategories([], ['lamb', 'fish', 'pork'])).toEqual(['lamb', 'fish', 'pork']);
|
||||||
|
});
|
||||||
|
it('works if the second list is not given', () => {
|
||||||
|
expect(combineCategories(['fish', 'beef'])).toEqual(['fish', 'beef']);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,33 @@
|
|||||||
|
import combineContinuousDomains from '../../src/utils/combineContinuousDomains';
|
||||||
|
|
||||||
|
describe('combineContinuousDomains()', () => {
|
||||||
|
it('uses the fixedDomain if all values are defined', () => {
|
||||||
|
expect(combineContinuousDomains([1, 2, 3], [4, 5, 6])).toEqual([1, 2, 3]);
|
||||||
|
expect(combineContinuousDomains([1, 2], [4, 5])).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
describe('if both fixedDomain and inputDomain are of length two, uses the fixedDomain as boundary', () => {
|
||||||
|
describe('min only', () => {
|
||||||
|
it('exceeds bound', () => {
|
||||||
|
expect(combineContinuousDomains([1, null], [0, 10])).toEqual([1, 10]);
|
||||||
|
});
|
||||||
|
it('is within bound', () => {
|
||||||
|
expect(combineContinuousDomains([1, null], [2, 10])).toEqual([2, 10]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('max only', () => {
|
||||||
|
it('exceeds bound', () => {
|
||||||
|
expect(combineContinuousDomains([null, 5], [0, 10])).toEqual([0, 5]);
|
||||||
|
});
|
||||||
|
it('is within bound', () => {
|
||||||
|
expect(combineContinuousDomains([null, 5], [2, 4])).toEqual([2, 4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('returns inputDomain for invalid bound', () => {
|
||||||
|
expect(combineContinuousDomains([null, null], [2, 10])).toEqual([2, 10]);
|
||||||
|
expect(combineContinuousDomains([], [2, 10])).toEqual([2, 10]);
|
||||||
|
});
|
||||||
|
it('returns undefined if there is also no inputDomain', () => {
|
||||||
|
expect(combineContinuousDomains([null, null])).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user