mirror of
https://github.com/apache/superset.git
synced 2024-09-19 12:09:42 -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 { ScaleConfig, D3Scale } from '../../types/Scale';
|
||||
import parseDateTime from '../parseDateTime';
|
||||
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>(
|
||||
config: ScaleConfig<Output>,
|
||||
scale: D3Scale<Output>,
|
||||
domainFromDataset?: string[] | number[] | boolean[] | Date[],
|
||||
) {
|
||||
const { domain, reverse } = config;
|
||||
if (typeof domain !== 'undefined' && domain.length > 0) {
|
||||
const processedDomain = inferElementTypeFromUnionOfArrayTypes(domain);
|
||||
const { domain, reverse, type } = config;
|
||||
|
||||
// Only set domain if all items are defined
|
||||
if (isEveryElementDefined(processedDomain)) {
|
||||
const order = createOrderFunction(reverse);
|
||||
|
||||
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(
|
||||
(reverse ? processedDomain.slice().reverse() : processedDomain).map(d =>
|
||||
isDateTime(d) ? parseDateTime(d) : d,
|
||||
order(
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,13 @@ export interface CombinedScaleConfig<Output extends Value = Value>
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
@ -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