Skip to content

Commit

Permalink
feat(encodable): implement axis functions for ChannelEncoder (apache-…
Browse files Browse the repository at this point in the history
…superset#247)

* feat: add axis encoder

* test: add unit test

* fix: params

* refactor: rename

* fix: address comments

* fix: update import

* fix: error

* fix: lint
  • Loading branch information
kristw authored Nov 15, 2019
1 parent 6eb0b52 commit 2cf401b
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 9 deletions.
18 changes: 12 additions & 6 deletions packages/superset-ui-encodable/src/encoders/ChannelEncoder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { extent as d3Extent } from 'd3-array';
import { HasToString, IdentityFunction } from '../types/Base';
import { ChannelType, ChannelInput } from '../types/Channel';
import { PlainObject, Dataset } from '../types/Data';
import { ChannelDef } from '../types/ChannelDef';
import { Value } from '../types/VegaLite';
import { isTypedFieldDef, isValueDef } from '../typeGuards/ChannelDef';
import { isX, isY, isXOrY } from '../typeGuards/Channel';
import ChannelEncoderAxis from './ChannelEncoderAxis';
import createGetterFromChannelDef, { Getter } from '../parsers/createGetterFromChannelDef';
import completeChannelDef, {
CompleteChannelDef,
Expand All @@ -10,10 +15,6 @@ import completeChannelDef, {
import createFormatterFromChannelDef from '../parsers/format/createFormatterFromChannelDef';
import createScaleFromScaleConfig from '../parsers/scale/createScaleFromScaleConfig';
import identity from '../utils/identity';
import { HasToString, IdentityFunction } from '../types/Base';
import { isTypedFieldDef, isValueDef } from '../typeGuards/ChannelDef';
import { isX, isY, isXOrY } from '../typeGuards/Channel';
import { Value } from '../types/VegaLite';

type EncodeFunction<Output> = (value: ChannelInput | Output) => Output | null | undefined;

Expand All @@ -22,7 +23,8 @@ export default class ChannelEncoder<Def extends ChannelDef<Output>, Output exten
readonly channelType: ChannelType;
readonly originalDefinition: Def;
readonly definition: CompleteChannelDef<Output>;
readonly scale: false | ReturnType<typeof createScaleFromScaleConfig>;
readonly scale?: ReturnType<typeof createScaleFromScaleConfig>;
readonly axis?: ChannelEncoderAxis<Def, Output>;

private readonly getValue: Getter<Output>;
readonly encodeValue: IdentityFunction<ChannelInput | Output> | EncodeFunction<Output>;
Expand Down Expand Up @@ -54,8 +56,12 @@ export default class ChannelEncoder<Def extends ChannelDef<Output>, Output exten
: identity;
} else {
this.encodeValue = (value: ChannelInput) => scale(value);
this.scale = scale;
}

if (this.definition.axis) {
this.axis = new ChannelEncoderAxis(this);
}
this.scale = scale;
}

encodeDatum: {
Expand Down
55 changes: 55 additions & 0 deletions packages/superset-ui-encodable/src/encoders/ChannelEncoderAxis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import ChannelEncoder from './ChannelEncoder';
import createFormatterFromFieldTypeAndFormat from '../parsers/format/createFormatterFromFieldTypeAndFormat';
import { CompleteAxisConfig } from '../fillers/completeAxisConfig';
import { ChannelDef } from '../types/ChannelDef';
import { Value, isDateTime } from '../types/VegaLite';
import { CompleteFieldDef } from '../fillers/completeChannelDef';
import { ChannelInput } from '../types/Channel';
import { HasToString } from '../types/Base';
import parseDateTime from '../parsers/parseDateTime';
import inferElementTypeFromUnionOfArrayTypes from '../utils/inferElementTypeFromUnionOfArrayTypes';

export default class ChannelEncoderAxis<
Def extends ChannelDef<Output>,
Output extends Value = Value
> {
readonly channelEncoder: ChannelEncoder<Def, Output>;
readonly config: Exclude<CompleteAxisConfig, false>;
readonly formatValue: (value: ChannelInput | HasToString) => string;

constructor(channelEncoder: ChannelEncoder<Def, Output>) {
this.channelEncoder = channelEncoder;
this.config = channelEncoder.definition.axis as Exclude<CompleteAxisConfig, false>;
this.formatValue = createFormatterFromFieldTypeAndFormat(
(channelEncoder.definition as CompleteFieldDef<Output>).type,
this.config.format || '',
);
}

getTitle() {
return this.config.title;
}

hasTitle() {
const { title } = this.config;

return title !== null && typeof title !== 'undefined' && title !== '';
}

getTickLabels() {
const { tickCount, values } = this.config;

if (typeof values !== 'undefined') {
return inferElementTypeFromUnionOfArrayTypes(values).map(v =>
this.formatValue(isDateTime(v) ? parseDateTime(v) : v),
);
}

const { scale } = this.channelEncoder;
if (scale && 'domain' in scale) {
return ('ticks' in scale ? scale.ticks(tickCount) : scale.domain()).map(this.formatValue);
}

return [];
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { isDateTime } from 'vega-lite/build/src/datetime';
import { DateTime } from '../types/VegaLite';
import { DateTime, isDateTime } from '../types/VegaLite';
import parseDateTime from './parseDateTime';

export default function parseDateTimeIfPossible<T>(d: DateTime | T) {
Expand Down
2 changes: 1 addition & 1 deletion packages/superset-ui-encodable/src/types/VegaLite.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Types imported from vega-lite

export { ValueDef, Value } from 'vega-lite/build/src/channeldef';
export { DateTime } from 'vega-lite/build/src/datetime';
export { isDateTime, DateTime } from 'vega-lite/build/src/datetime';
export { SchemeParams, ScaleType, Scale, NiceTime } from 'vega-lite/build/src/scale';
export { Axis } from 'vega-lite/build/src/axis';
export { Type } from 'vega-lite/build/src/type';
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { ChannelEncoder } from '../../src';

describe('ChannelEncoderAxis', () => {
describe('new ChannelEncoderAxis(channelEncoder)', () => {
it('completes the definition and creates an encoder for it', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
},
});
expect(encoder.axis).toBeDefined();
});
});

describe('.formatValue()', () => {
it('formats value', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
axis: {
format: '.2f',
},
},
});
expect(encoder.axis && encoder.axis.formatValue(200)).toEqual('200.00');
});
it('fallsback to field formatter', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
format: '.3f',
},
});
expect(encoder.axis && encoder.axis.formatValue(200)).toEqual('200.000');
});
it('fallsback to default formatter', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
},
});
expect(encoder.axis && encoder.axis.formatValue(200)).toEqual('200');
});
});

describe('.getTitle()', () => {
it('returns the axis title', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
title: 'Speed',
axis: {
title: 'Speed!',
},
},
});
expect(encoder.axis && encoder.axis.getTitle()).toEqual('Speed!');
});
it('returns the field title when not specified', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
title: 'Speed',
},
});
expect(encoder.axis && encoder.axis.getTitle()).toEqual('Speed');
});
it('returns the field name when no title is specified', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
},
});
expect(encoder.axis && encoder.axis.getTitle()).toEqual('speed');
});
});

describe('.hasTitle()', () => {
it('returns true if the title is not empty', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
title: 'Speed',
axis: {
title: 'Speed!',
},
},
});
expect(encoder.axis && encoder.axis.hasTitle()).toBeTruthy();
});
it('returns false otherwise', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
title: 'Speed',
axis: {
title: '',
},
},
});
expect(encoder.axis && encoder.axis.hasTitle()).toBeFalsy();
});
});

describe('.getTickLabels()', () => {
it('handles hard-coded tick values', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
axis: {
values: [1, 2, 3],
},
},
});
expect(encoder.axis && encoder.axis.getTickLabels()).toEqual(['1', '2', '3']);
});
it('handles hard-coded DateTime object', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'temporal',
field: 'time',
axis: {
format: '%Y',
values: [{ year: 2018 }, { year: 2019 }],
},
},
});
expect(encoder.axis && encoder.axis.getTickLabels()).toEqual(['2018', '2019']);
});
describe('uses information from scale', () => {
it('uses ticks when available', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
scale: {
type: 'linear',
domain: [0, 100],
},
axis: {
tickCount: 5,
},
},
});
expect(encoder.axis && encoder.axis.getTickLabels()).toEqual([
'0',
'20',
'40',
'60',
'80',
'100',
]);
});
it('or uses domain', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'nominal',
field: 'brand',
scale: {
domain: ['honda', 'toyota'],
},
},
});
expect(encoder.axis && encoder.axis.getTickLabels()).toEqual(['honda', 'toyota']);
});
});
it('returns empty array otherwise', () => {
const encoder = new ChannelEncoder({
name: 'x',
channelType: 'X',
definition: {
type: 'quantitative',
field: 'speed',
scale: false,
},
});
expect(encoder.axis && encoder.axis.getTickLabels()).toEqual([]);
});
});
});

0 comments on commit 2cf401b

Please sign in to comment.