Skip to content

Commit

Permalink
feat: Add channel encoder (apache#224)
Browse files Browse the repository at this point in the history
* feat: add channel encoder

* fix: all errors

* fix: test

* feat: complete channel encoder implementation and unit tests

* fix: lint

* fix: address comments

* fix: lint
  • Loading branch information
kristw authored Sep 25, 2019
1 parent 60ce139 commit d4f76b8
Show file tree
Hide file tree
Showing 16 changed files with 2,493 additions and 27 deletions.
2 changes: 1 addition & 1 deletion packages/superset-ui-color/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"access": "public"
},
"dependencies": {
"@types/d3-scale": "^2.0.2",
"@types/d3-scale": "^2.1.1",
"d3-scale": "^3.0.0"
},
"peerDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion packages/superset-ui-encodable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
},
"private": true,
"dependencies": {
"@types/d3-array": "^2.0.0",
"@types/d3-interpolate": "^1.3.1",
"@types/d3-scale": "^2.1.1",
"@types/d3-time": "^1.0.10",
"d3-array": "^2.3.1",
"d3-interpolate": "^1.3.2",
"d3-scale": "^3.0.1",
"d3-time": "^1.0.11",
Expand Down
129 changes: 129 additions & 0 deletions packages/superset-ui-encodable/src/encoders/ChannelEncoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { extent as d3Extent } from 'd3-array';
import { ChannelType, ChannelInput } from '../types/Channel';
import { PlainObject, Dataset } from '../types/Data';
import { ChannelDef } from '../types/ChannelDef';
import createGetterFromChannelDef, { Getter } from '../parsers/createGetterFromChannelDef';
import completeChannelDef, { CompleteChannelDef } from '../fillers/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;

export default class ChannelEncoder<Def extends ChannelDef<Output>, Output extends Value = Value> {
readonly name: string | Symbol | number;
readonly channelType: ChannelType;
readonly originalDefinition: Def;
readonly definition: CompleteChannelDef<Output>;
readonly scale: false | ReturnType<typeof createScaleFromScaleConfig>;

private readonly getValue: Getter<Output>;
readonly encodeValue: IdentityFunction<ChannelInput | Output> | EncodeFunction<Output>;
readonly formatValue: (value: ChannelInput | HasToString) => string;

constructor({
name,
channelType,
definition: originalDefinition,
}: {
name: string;
channelType: ChannelType;
definition: Def;
}) {
this.name = name;
this.channelType = channelType;

this.originalDefinition = originalDefinition;
this.definition = completeChannelDef(this.channelType, originalDefinition);

this.getValue = createGetterFromChannelDef(this.definition);
this.formatValue = createFormatterFromChannelDef(this.definition);

const scale = this.definition.scale && createScaleFromScaleConfig(this.definition.scale);
this.encodeValue = scale === false ? identity : (value: ChannelInput) => scale(value);
this.scale = scale;
}

encodeDatum: {
(datum: PlainObject): Output | null | undefined;
(datum: PlainObject, otherwise: Output): Output;
} = (datum: PlainObject, otherwise?: Output) => {
const value = this.getValueFromDatum(datum);

if (otherwise !== undefined && (value === null || value === undefined)) {
return otherwise;
}

return this.encodeValue(value) as Output;
};

formatDatum = (datum: PlainObject): string => this.formatValue(this.getValueFromDatum(datum));

getValueFromDatum = <T extends ChannelInput | Output>(datum: PlainObject, otherwise?: T) => {
const value = this.getValue(datum);

return otherwise !== undefined && (value === null || value === undefined)
? otherwise
: (value as T);
};

getDomain = (data: Dataset) => {
if (isValueDef(this.definition)) {
const { value } = this.definition;

return [value];
}

const { type } = this.definition;
if (type === 'nominal' || type === 'ordinal') {
return Array.from(new Set(data.map(d => this.getValueFromDatum(d)))) as string[];
} else if (type === 'quantitative') {
const extent = d3Extent(data, d => this.getValueFromDatum<number>(d));

return typeof extent[0] === 'undefined' ? [0, 1] : (extent as [number, number]);
} else if (type === 'temporal') {
const extent = d3Extent(data, d => this.getValueFromDatum<number | Date>(d));

return typeof extent[0] === 'undefined'
? [0, 1]
: (extent as [number, number] | [Date, Date]);
}

return [];
};

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

isGroupBy() {
if (isTypedFieldDef(this.definition)) {
const { type } = this.definition;

return (
this.channelType === 'Category' ||
this.channelType === 'Text' ||
(this.channelType === 'Color' && (type === 'nominal' || type === 'ordinal')) ||
(isXOrY(this.channelType) && (type === 'nominal' || type === 'ordinal'))
);
}

return false;
}

isX() {
return isX(this.channelType);
}

isXOrY() {
return isXOrY(this.channelType);
}

isY() {
return isY(this.channelType);
}
}
36 changes: 30 additions & 6 deletions packages/superset-ui-encodable/src/fillers/completeChannelDef.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
import { ChannelDef } from '../types/ChannelDef';
import { ChannelDef, NonValueDef } from '../types/ChannelDef';
import { ChannelType } from '../types/Channel';
import { isFieldDef } from '../typeGuards/ChannelDef';
import { isFieldDef, isValueDef, isTypedFieldDef } from '../typeGuards/ChannelDef';
import completeAxisConfig, { CompleteAxisConfig } from './completeAxisConfig';
import completeScaleConfig, { CompleteScaleConfig } from './completeScaleConfig';
import { Value } from '../types/VegaLite';
import { Value, ValueDef, Type } from '../types/VegaLite';
import inferFieldType from './inferFieldType';

type CompleteChannelDef<Output extends Value = Value> = Omit<
ChannelDef,
export interface CompleteValueDef<Output extends Value = Value> extends ValueDef<Output> {
axis: false;
scale: false;
title: '';
}

export type CompleteFieldDef<Output extends Value = Value> = Omit<
NonValueDef<Output>,
'title' | 'axis' | 'scale'
> & {
type: Type;
axis: CompleteAxisConfig;
scale: CompleteScaleConfig<Output>;
title: string;
};

export default function completeChannelDef<Output extends Value = Value>(
export type CompleteChannelDef<Output extends Value = Value> =
| CompleteValueDef<Output>
| CompleteFieldDef<Output>;

export default function completeChannelDef<Output extends Value>(
channelType: ChannelType,
channelDef: ChannelDef<Output>,
): CompleteChannelDef<Output> {
if (isValueDef(channelDef)) {
return {
...channelDef,
axis: false,
scale: false,
title: '',
};
}

// Fill top-level properties
const copy = {
...channelDef,
title: isFieldDef(channelDef) ? channelDef.title || channelDef.field : '',
type: isTypedFieldDef(channelDef)
? channelDef.type
: inferFieldType(channelType, channelDef.field),
};

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Value } from '../types/VegaLite';

export type CompleteScaleConfig<Output extends Value = Value> = false | ScaleConfig<Output>;

export default function completeScaleConfig<Output extends Value = Value>(
export default function completeScaleConfig<Output extends Value>(
channelType: ChannelType,
channelDef: ChannelDef<Output>,
): CompleteScaleConfig<Output> {
Expand Down
13 changes: 13 additions & 0 deletions packages/superset-ui-encodable/src/fillers/inferFieldType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ChannelType } from '../types/Channel';
import { isXOrY } from '../typeGuards/Channel';
import { Type } from '../types/VegaLite';

const temporalFieldNames = new Set(['time', 'date', 'datetime', 'timestamp']);

export default function inferFieldType(channelType: ChannelType, field: string = ''): Type {
if (isXOrY(channelType) || channelType === 'Numeric') {
return temporalFieldNames.has(field.toLowerCase()) ? 'temporal' : 'quantitative';
}

return 'nominal';
}
3 changes: 3 additions & 0 deletions packages/superset-ui-encodable/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as ChannelEncoder } from './encoders/ChannelEncoder';
export { default as completeChannelDef } from './fillers/completeChannelDef';
export { default as createScaleFromScaleConfig } from './parsers/scale/createScaleFromScaleConfig';
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { get } from 'lodash/fp';
import identity from '../utils/identity';
import { ChannelDef } from '../types/ChannelDef';
import { isValueDef } from '../typeGuards/ChannelDef';
import { PlainObject } from '../types/Data';
import { Value } from '../types/VegaLite';
import { ChannelInput } from '../types/Channel';

export default function createGetterFromChannelDef(
definition: ChannelDef,
): (x?: PlainObject) => any {
export type Getter<Output extends Value> = (x?: PlainObject) => ChannelInput | Output | undefined;

export default function createGetterFromChannelDef<Output extends Value>(
definition: ChannelDef<Output>,
): Getter<Output> {
if (isValueDef(definition)) {
return () => definition.value;
} else if (typeof definition.field !== 'undefined') {
return get(definition.field);
}

return identity;
return () => undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
scalePoint,
scaleBand,
} from 'd3-scale';
import { HasToString } from '../../types/Base';
import { ScaleConfig } from '../../types/Scale';
import { ScaleConfig, CategoricalScaleInput } from '../../types/Scale';
import { ScaleType, Value } from '../../types/VegaLite';

// eslint-disable-next-line complexity
Expand Down Expand Up @@ -44,11 +43,11 @@ export default function createScaleFromScaleType<Output extends Value>(
case ScaleType.THRESHOLD:
return scaleThreshold<number | string | Date, Output>();
case ScaleType.ORDINAL:
return scaleOrdinal<HasToString, Output>();
return scaleOrdinal<CategoricalScaleInput, Output>();
case ScaleType.POINT:
return scalePoint<HasToString>();
return scalePoint<CategoricalScaleInput>();
case ScaleType.BAND:
return scaleBand<HasToString>();
return scaleBand<CategoricalScaleInput>();
case ScaleType.SYMLOG:
// TODO: d3-scale typings does not include scaleSymlog yet
// needs to patch the declaration file before continue.
Expand Down
3 changes: 3 additions & 0 deletions packages/superset-ui-encodable/src/types/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ export type RequiredSome<T, RequiredFields extends keyof T> = {
{
[Field in RequiredFields]-?: T[Field];
};

/** Signature of an identity function */
export type IdentityFunction<T> = (value: T) => T;
16 changes: 9 additions & 7 deletions packages/superset-ui-encodable/src/types/Scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ export interface WithScale<Output extends Value = Value> {
/** Each ScaleCategory contains one or more ScaleType */
export type ScaleCategory = 'continuous' | 'discrete' | 'discretizing';

export type CategoricalScaleInput = HasToString | null | undefined;

export interface ScaleTypeToD3ScaleType<Output extends Value = Value> {
[ScaleType.LINEAR]: ScaleLinear<Output, Output>;
[ScaleType.LOG]: ScaleLogarithmic<Output, Output>;
Expand All @@ -202,10 +204,10 @@ export interface ScaleTypeToD3ScaleType<Output extends Value = Value> {
[ScaleType.QUANTILE]: ScaleQuantile<Output>;
[ScaleType.QUANTIZE]: ScaleQuantize<Output>;
[ScaleType.THRESHOLD]: ScaleThreshold<number | string | Date, Output>;
[ScaleType.BIN_ORDINAL]: ScaleOrdinal<HasToString, Output>;
[ScaleType.ORDINAL]: ScaleOrdinal<HasToString, Output>;
[ScaleType.POINT]: ScalePoint<HasToString>;
[ScaleType.BAND]: ScaleBand<HasToString>;
[ScaleType.BIN_ORDINAL]: ScaleOrdinal<CategoricalScaleInput, Output>;
[ScaleType.ORDINAL]: ScaleOrdinal<CategoricalScaleInput, Output>;
[ScaleType.POINT]: ScalePoint<CategoricalScaleInput>;
[ScaleType.BAND]: ScaleBand<CategoricalScaleInput>;
}

export type ContinuousD3Scale<Output extends Value = Value> =
Expand All @@ -219,6 +221,6 @@ export type D3Scale<Output extends Value = Value> =
| ScaleQuantile<Output>
| ScaleQuantize<Output>
| ScaleThreshold<number | string | Date, Output>
| ScaleOrdinal<HasToString, Output>
| ScalePoint<HasToString>
| ScaleBand<HasToString>;
| ScaleOrdinal<CategoricalScaleInput, Output>
| ScalePoint<CategoricalScaleInput>
| ScaleBand<CategoricalScaleInput>;
Loading

0 comments on commit d4f76b8

Please sign in to comment.