-
Notifications
You must be signed in to change notification settings - Fork 13.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
497a4b0
commit f5f944b
Showing
13 changed files
with
294 additions
and
12 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
...ui/superset-ui/packages/superset-ui-encodable/src/parsers/domain/parseContinuousDomain.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
10 changes: 10 additions & 0 deletions
10
...t_ui/superset-ui/packages/superset-ui-encodable/src/parsers/domain/parseDiscreteDomain.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}`); | ||
} |
7 changes: 7 additions & 0 deletions
7
...rset_ui/superset-ui/packages/superset-ui-encodable/src/parsers/parseDateTimeIfPossible.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
52 changes: 42 additions & 10 deletions
52
...y_superset_ui/superset-ui/packages/superset-ui-encodable/src/parsers/scale/applyDomain.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
const order = createOrderFunction(reverse); | ||
|
||
const inputDomain = | ||
domainFromDataset && domainFromDataset.length | ||
? inferElementTypeFromUnionOfArrayTypes(domainFromDataset) | ||
: undefined; | ||
|
||
// Only set domain if all items are defined | ||
if (isEveryElementDefined(processedDomain)) { | ||
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))); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
...ary_superset_ui/superset-ui/packages/superset-ui-encodable/src/utils/combineCategories.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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))); | ||
} |
39 changes: 39 additions & 0 deletions
39
...erset_ui/superset-ui/packages/superset-ui-encodable/src/utils/combineContinuousDomains.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
27 changes: 27 additions & 0 deletions
27
...erset-ui/packages/superset-ui-encodable/test/parsers/domain/parseContinuousDomain.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]); | ||
}); | ||
}); |
9 changes: 9 additions & 0 deletions
9
...uperset-ui/packages/superset-ui-encodable/test/parsers/domain/parseDiscreteDomain.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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']); | ||
}); | ||
}); |
56 changes: 56 additions & 0 deletions
56
...rset_ui/superset-ui/packages/superset-ui-encodable/test/parsers/scale/applyDomain.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]); | ||
}); | ||
}); |
18 changes: 18 additions & 0 deletions
18
...perset_ui/superset-ui/packages/superset-ui-encodable/test/utils/combineCategories.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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']); | ||
}); | ||
}); |
33 changes: 33 additions & 0 deletions
33
...ui/superset-ui/packages/superset-ui-encodable/test/utils/combineContinuousDomains.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
}); | ||
}); |