From 9ef831829b9630593f7320ca840813a2d8a91df1 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Thu, 14 Nov 2019 16:04:54 -0800 Subject: [PATCH] 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 --- .../src/encoders/ChannelEncoder.ts | 18 +- .../src/encoders/ChannelEncoderAxis.ts | 55 +++++ .../src/parsers/parseDateTimeIfPossible.ts | 3 +- .../src/types/VegaLite.ts | 2 +- .../test/encoders/ChannelEncoderAxis.test.ts | 216 ++++++++++++++++++ 5 files changed, 285 insertions(+), 9 deletions(-) create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/encoders/ChannelEncoderAxis.ts create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/test/encoders/ChannelEncoderAxis.test.ts diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/encoders/ChannelEncoder.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/encoders/ChannelEncoder.ts index 673df640cad5..567ad8f3b88a 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/encoders/ChannelEncoder.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/encoders/ChannelEncoder.ts @@ -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 = (value: ChannelInput | Output) => Output | null | undefined; @@ -22,7 +23,8 @@ export default class ChannelEncoder, Output exten readonly channelType: ChannelType; readonly originalDefinition: Def; readonly definition: CompleteChannelDef; - readonly scale: false | ReturnType; + readonly scale?: ReturnType; + readonly axis?: ChannelEncoderAxis; private readonly getValue: Getter; readonly encodeValue: IdentityFunction | EncodeFunction; @@ -54,8 +56,12 @@ export default class ChannelEncoder, 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: { diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/encoders/ChannelEncoderAxis.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/encoders/ChannelEncoderAxis.ts new file mode 100644 index 000000000000..9c19ccc8047a --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/encoders/ChannelEncoderAxis.ts @@ -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 extends Value = Value +> { + readonly channelEncoder: ChannelEncoder; + readonly config: Exclude; + readonly formatValue: (value: ChannelInput | HasToString) => string; + + constructor(channelEncoder: ChannelEncoder) { + this.channelEncoder = channelEncoder; + this.config = channelEncoder.definition.axis as Exclude; + this.formatValue = createFormatterFromFieldTypeAndFormat( + (channelEncoder.definition as CompleteFieldDef).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 []; + } +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/parsers/parseDateTimeIfPossible.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/parsers/parseDateTimeIfPossible.ts index ccb980bec6a4..b5989aa74e4f 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/parsers/parseDateTimeIfPossible.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/parsers/parseDateTimeIfPossible.ts @@ -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(d: DateTime | T) { diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/types/VegaLite.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/types/VegaLite.ts index eecebd26a25f..fd74b6866056 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/types/VegaLite.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/src/types/VegaLite.ts @@ -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'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/test/encoders/ChannelEncoderAxis.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/test/encoders/ChannelEncoderAxis.test.ts new file mode 100644 index 000000000000..cbcc8ce0d6e3 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-encodable/test/encoders/ChannelEncoderAxis.test.ts @@ -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([]); + }); + }); +});