Skip to content

Commit

Permalink
feat: Add channel encoder (#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 and zhaoyongjie committed Nov 26, 2021
1 parent 1f70765 commit 937a7ec
Show file tree
Hide file tree
Showing 16 changed files with 2,493 additions and 27 deletions.
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
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
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);
}
}
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
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';
}
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
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;
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>;

0 comments on commit 937a7ec

Please sign in to comment.