mirror of
https://github.com/apache/superset.git
synced 2024-09-19 12:09:42 -04:00
feat(encodable): implement axis functions for ChannelEncoder (#247)
* feat: add axis encoder * test: add unit test * fix: params * refactor: rename * fix: address comments * fix: update import * fix: error * fix: lint
This commit is contained in:
parent
f5f944b405
commit
9ef831829b
@ -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,
|
||||
@ -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;
|
||||
|
||||
@ -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>;
|
||||
@ -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: {
|
||||
|
@ -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 [];
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user