feat(encodable): add function for setting domain (#256)

* feat: add scale helper

* feat: add functions

* fix: remove unused import

* fix: add unit tests

* fix: unit tes
This commit is contained in:
Krist Wongsuphasawat 2019-11-15 11:43:03 -08:00 committed by Yongjie Zhao
parent e719c19365
commit 1772b671cf
6 changed files with 142 additions and 12 deletions

View File

@ -13,8 +13,12 @@ import completeChannelDef, {
CompleteValueDef,
} from '../fillers/completeChannelDef';
import createFormatterFromChannelDef from '../parsers/format/createFormatterFromChannelDef';
import createScaleFromScaleConfig from '../parsers/scale/createScaleFromScaleConfig';
import identity from '../utils/identity';
import applyDomain from '../parsers/scale/applyDomain';
import applyZero from '../parsers/scale/applyZero';
import applyNice from '../parsers/scale/applyNice';
import { AllScale } from '../types/Scale';
import { createScaleFromScaleConfig } from '..';
type EncodeFunction<Output> = (value: ChannelInput | Output) => Output | null | undefined;
@ -23,7 +27,7 @@ export default class ChannelEncoder<Def extends ChannelDef<Output>, Output exten
readonly channelType: ChannelType;
readonly originalDefinition: Def;
readonly definition: CompleteChannelDef<Output>;
readonly scale?: ReturnType<typeof createScaleFromScaleConfig>;
readonly scale?: AllScale<Output>;
readonly axis?: ChannelEncoderAxis<Def, Output>;
private readonly getValue: Getter<Output>;
@ -48,15 +52,15 @@ export default class ChannelEncoder<Def extends ChannelDef<Output>, Output exten
this.getValue = createGetterFromChannelDef(this.definition);
this.formatValue = createFormatterFromChannelDef(this.definition);
const scale = this.definition.scale && createScaleFromScaleConfig(this.definition.scale);
if (scale === false) {
if (this.definition.scale) {
const scale = createScaleFromScaleConfig(this.definition.scale);
this.encodeValue = (value: ChannelInput) => scale(value);
this.scale = scale;
} else {
this.encodeValue =
'value' in this.definition
? () => (this.definition as CompleteValueDef<Output>).value
: identity;
} else {
this.encodeValue = (value: ChannelInput) => scale(value);
this.scale = scale;
}
if (this.definition.axis) {
@ -112,6 +116,21 @@ export default class ChannelEncoder<Def extends ChannelDef<Output>, Output exten
return [];
};
setDomain(domain: ChannelInput[]) {
if (this.definition.scale !== false && this.scale && 'domain' in this.scale) {
const config = this.definition.scale;
applyDomain(config, this.scale, domain);
applyZero(config, this.scale);
applyNice(config, this.scale);
}
return this;
}
setDomainFromDataset(data: Dataset) {
return this.scale ? this.setDomain(this.getDomainFromDataset(data)) : this;
}
getTitle() {
return this.definition.title;
}

View File

@ -7,15 +7,17 @@ import parseDateTimeIfPossible from '../parseDateTimeIfPossible';
import parseContinuousDomain from '../domain/parseContinuousDomain';
import parseDiscreteDomain from '../domain/parseDiscreteDomain';
import combineContinuousDomains from '../../utils/combineContinuousDomains';
import { ChannelInput } from '../../types/Channel';
import removeUndefinedAndNull from '../../utils/removeUndefinedAndNull';
function createOrderFunction(reverse: boolean | undefined) {
return reverse ? <T>(array: T[]) => array.slice().reverse() : <T>(array: T[]) => array;
return reverse ? <T>(array: T[]) => array.concat().reverse() : <T>(array: T[]) => array;
}
export default function applyDomain<Output extends Value>(
config: ScaleConfig<Output>,
scale: D3Scale<Output>,
domainFromDataset?: string[] | number[] | boolean[] | Date[],
domainFromDataset?: ChannelInput[],
) {
const { domain, reverse, type } = config;
@ -32,7 +34,7 @@ export default function applyDomain<Output extends Value>(
if (isContinuousScale(scale, type)) {
const combined = combineContinuousDomains(
parseContinuousDomain(fixedDomain, type),
inputDomain && parseContinuousDomain(inputDomain, type),
inputDomain && removeUndefinedAndNull(parseContinuousDomain(inputDomain, type)),
);
if (combined) {
scale.domain(order(combined));
@ -49,7 +51,7 @@ export default function applyDomain<Output extends Value>(
}
} else if (inputDomain) {
if (isContinuousScale(scale, type)) {
scale.domain(order(parseContinuousDomain(inputDomain, type)));
scale.domain(order(removeUndefinedAndNull(parseContinuousDomain(inputDomain, type))));
} else {
scale.domain(order(parseDiscreteDomain(inputDomain)));
}

View File

@ -230,3 +230,5 @@ export type D3Scale<Output extends Value = Value> =
| ScaleOrdinal<CategoricalScaleInput, Output>
| ScalePoint<CategoricalScaleInput>
| ScaleBand<CategoricalScaleInput>;
export type AllScale<Output extends Value = Value> = D3Scale<Output> | ((val?: any) => string);

View File

@ -0,0 +1,6 @@
export default function removeUndefinedAndNull<T>(array: T[]) {
return array.filter(x => typeof x !== 'undefined' && x !== null) as Exclude<
T,
undefined | null
>[];
}

View File

@ -117,7 +117,7 @@ describe('ChannelEncoder', () => {
});
});
describe('.getDomain()', () => {
describe('.getDomainFromDataset()', () => {
describe('for ValueDef', () => {
it('returns an array of fixed value', () => {
const encoder = new ChannelEncoder({
@ -232,6 +232,93 @@ describe('ChannelEncoder', () => {
});
});
describe('.setDomain()', () => {
it('sets the domain', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
title: 'Speed',
scale: { zero: false },
},
});
expect(encoder.setDomain([20, 30])).toEqual(encoder);
expect('domain' in encoder.scale && encoder.scale!.domain()).toEqual([20, 30]);
});
it('sets the domain (with zero)', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
title: 'Speed',
},
});
expect(encoder.setDomain([20, 30])).toEqual(encoder);
expect('domain' in encoder.scale && encoder.scale!.domain()).toEqual([0, 30]);
});
it('sets the domain (with nice)', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
title: 'Speed',
scale: { zero: false },
},
});
expect(encoder.setDomain([21.5, 30])).toEqual(encoder);
expect('domain' in encoder.scale && encoder.scale!.domain()).toEqual([21, 30]);
});
it('does nothing if does not have scale', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
title: 'Speed',
scale: false,
},
});
expect(encoder.setDomain([21.5, 30])).toEqual(encoder);
expect(encoder.scale).toBeUndefined();
});
});
describe('.setDomainFromDataset()', () => {
it('sets the domain', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'price',
scale: { zero: false },
},
});
expect(encoder.setDomainFromDataset([{ price: 1 }, { price: 5 }])).toEqual(encoder);
expect('domain' in encoder.scale && encoder.scale!.domain()).toEqual([1, 5]);
});
it('does nothing if does not have scale', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'price',
scale: false,
},
});
expect(encoder.setDomainFromDataset([{ price: 1 }, { price: 5 }])).toEqual(encoder);
expect(encoder.scale).toBeUndefined();
});
});
describe('.getTitle()', () => {
it('returns title', () => {
const encoder = new ChannelEncoder({

View File

@ -19,6 +19,20 @@ describe('applyDomain()', () => {
);
expect(scale.domain()).toEqual(['a', 'c', 'b']);
});
it('continuous domain (reverse)', () => {
const scale = scaleLinear();
applyDomain({ type: 'linear', domain: [null, 10], reverse: true }, scale, [1, 20]);
expect(scale.domain()).toEqual([10, 1]);
});
it('discrete domain (reverse)', () => {
const scale = scaleOrdinal<HasToString, string>();
applyDomain(
{ type: 'ordinal', domain: ['a', 'c'], range: ['red', 'green', 'blue'], reverse: true },
scale,
['a', 'b', 'c'],
);
expect(scale.domain()).toEqual(['b', 'c', 'a']);
});
});
describe('without domainFromDataset', () => {
it('continuous domain', () => {