Skip to content
This repository has been archived by the owner on Dec 10, 2021. It is now read-only.

Commit

Permalink
feat: add encodeable utilities for chart (#15)
Browse files Browse the repository at this point in the history
* feat: add encodeable utilities

* feat: add types back

* refactor: simplify function calls

* refactor: rename generic type

* refactor: more edits

* refactor: remove unused function

* refactor: rename file

* fix: address comments

* fix: add vega back
  • Loading branch information
kristw committed Mar 20, 2019
1 parent 5746975 commit 3f68efb
Show file tree
Hide file tree
Showing 19 changed files with 556 additions and 4 deletions.
9 changes: 8 additions & 1 deletion plugins/superset-ui-plugins/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,14 @@
},
"prettier",
"typescript"
]
],
"typescript": {
"compilerOptions": {
"typeRoots": [
"../../node_modules/vega-lite/typings"
]
}
}
},
"workspaces": [
"./packages/*"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,27 @@
"dependencies": {
"@data-ui/theme": "^0.0.75",
"@data-ui/xy-chart": "^0.0.76",
"@types/d3-scale": "^2.1.1",
"@vx/axis": "^0.0.184",
"@vx/group": "^0.0.183",
"@vx/legend": "^0.0.183",
"@vx/responsive": "^0.0.184",
"@vx/scale": "^0.0.182",
"@vx/shape": "^0.0.184",
"csstype": "^2.6.3",
"d3-array": "^2.0.3",
"d3-scale": "^2.2.2",
"prop-types": "^15.6.2"
"lodash": "^4.17.11",
"prop-types": "^15.6.2",
"reselect" : "^4.0.0",
"vega": "^5.2.0",
"vega-lite": "^3.0.0-rc15"
},
"peerDependencies": {
"@superset-ui/chart": "^0.10.0",
"@superset-ui/chart": "^0.10.2",
"@superset-ui/color": "^0.10.0",
"@superset-ui/core": "^0.10.0",
"@superset-ui/dimension": "^0.10.0",
"@superset-ui/dimension": "^0.10.4",
"@superset-ui/number-format": "^0.10.0",
"@superset-ui/time-format": "^0.10.0",
"@superset-ui/translation": "^0.10.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { MarkPropChannelDef, XFieldDef, YFieldDef } from '../encodeable/types/FieldDef';
import AbstractEncoder from '../encodeable/AbstractEncoder';
import { PartialSpec } from '../encodeable/types/Specification';

/**
* Define output type for each channel
*/
export interface Outputs {
x: number | null;
y: number | null;
color: string;
fill: boolean;
strokeDasharray: string;
}

/**
* Define encoding config for each channel
*/
export interface Encoding {
x: XFieldDef<Outputs['x']>;
y: YFieldDef<Outputs['y']>;
color: MarkPropChannelDef<Outputs['color']>;
fill: MarkPropChannelDef<Outputs['fill']>;
strokeDasharray: MarkPropChannelDef<Outputs['strokeDasharray']>;
}

export default class Encoder extends AbstractEncoder<Outputs, Encoding> {
static DEFAULT_ENCODINGS: Encoding = {
color: { value: '#222' },
fill: { value: false },
strokeDasharray: { value: '' },
x: { field: 'x', type: 'quantitative' },
y: { field: 'y', type: 'quantitative' },
};

constructor(spec: PartialSpec<Encoding>) {
super(spec, Encoder.DEFAULT_ENCODINGS);
}

createChannels() {
return {
color: this.createChannel('color'),
fill: this.createChannel('fill', { legend: false }),
strokeDasharray: this.createChannel('strokeDasharray'),
x: this.createChannel('x'),
y: this.createChannel('y'),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Value } from 'vega-lite/build/src/fielddef';
import ChannelEncoder from './ChannelEncoder';
import { ChannelOptions } from './types/Channel';
import { ChannelDef, isFieldDef } from './types/FieldDef';
import { FullSpec, BaseOptions, PartialSpec } from './types/Specification';

export type ObjectWithKeysFromAndValueType<T extends {}, V> = { [key in keyof T]: V };

export type ChannelOutputs<T> = ObjectWithKeysFromAndValueType<T, Value>;

export type BaseEncoding<Output extends ObjectWithKeysFromAndValueType<Output, Value>> = {
[key in keyof Output]: ChannelDef<Output[key]>
};

export type Channels<
Outputs extends ChannelOutputs<Encoding>,
Encoding extends BaseEncoding<Outputs>
> = { readonly [k in keyof Outputs]: ChannelEncoder<Encoding[k], Outputs[k]> };

export default abstract class AbstractEncoder<
Outputs extends ChannelOutputs<Encoding>,
Encoding extends BaseEncoding<Outputs>,
Options extends BaseOptions = BaseOptions
> {
readonly spec: FullSpec<Encoding, Options>;
readonly channels: Channels<Outputs, Encoding>;

readonly legends: {
[key: string]: (keyof Encoding)[];
};

constructor(spec: PartialSpec<Encoding, Options>, defaultEncoding?: Encoding) {
this.spec = this.createFullSpec(spec, defaultEncoding);
this.channels = this.createChannels();
this.legends = {};

// Group the channels that use the same field together
// so they can share the same legend.
(Object.keys(this.channels) as (keyof Encoding)[])
.map((key: keyof Encoding) => this.channels[key])
.filter(c => c.hasLegend())
.forEach(c => {
if (isFieldDef(c.definition)) {
const key = c.name as keyof Encoding;
const { field } = c.definition;
if (this.legends[field]) {
this.legends[field].push(key);
} else {
this.legends[field] = [key];
}
}
});
}

/**
* subclass can override this
*/
protected createFullSpec(spec: PartialSpec<Encoding, Options>, defaultEncoding?: Encoding) {
if (typeof defaultEncoding === 'undefined') {
return spec as FullSpec<Encoding, Options>;
}

const { encoding, ...rest } = spec;

return {
...rest,
encoding: {
...defaultEncoding,
...encoding,
},
};
}

protected createChannel<ChannelName extends keyof Outputs>(
name: ChannelName,
options?: ChannelOptions,
) {
const { encoding } = this.spec;

return new ChannelEncoder<Encoding[ChannelName], Outputs[ChannelName]>(
`${name}`,
encoding[name],
{
...this.spec.options,
...options,
},
);
}

/**
* subclass should override this
*/
protected abstract createChannels(): Channels<Outputs, Encoding>;

hasLegend() {
return Object.keys(this.legends).length > 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Value } from 'vega-lite/build/src/fielddef';
import { CategoricalColorScale } from '@superset-ui/color';
import { ScaleOrdinal } from 'd3-scale';
import { TimeFormatter } from '@superset-ui/time-format';
import { NumberFormatter } from '@superset-ui/number-format';
import {
ChannelDef,
Formatter,
isScaleFieldDef,
isMarkPropFieldDef,
isValueDef,
} from './types/FieldDef';
import { PlainObject } from './types/Data';
import extractScale from './parsers/extractScale';
import extractGetter from './parsers/extractGetter';
import extractFormat from './parsers/extractFormat';
import extractAxis, { isXYChannel } from './parsers/extractAxis';
import isEnabled from './utils/isEnabled';
import isDisabled from './utils/isDisabled';
import { ChannelOptions } from './types/Channel';
import identity from './utils/identity';

export default class ChannelEncoder<Def extends ChannelDef<Output>, Output extends Value = Value> {
readonly name: string;
readonly definition: Def;
readonly options: ChannelOptions;

readonly axis?: PlainObject;
protected readonly getValue: (datum: PlainObject) => Value;
readonly scale?: ScaleOrdinal<string, Output> | CategoricalColorScale | ((x: any) => Output);
readonly formatter: Formatter;

readonly encodeValue: (value: any) => Output;
readonly formatValue: (value: any) => string;

constructor(name: string, definition: Def, options: ChannelOptions = {}) {
this.name = name;
this.definition = definition;
this.options = options;

this.getValue = extractGetter(definition);

const formatter = extractFormat(definition);
this.formatter = formatter;
if (formatter instanceof NumberFormatter) {
this.formatValue = (value: any) => formatter(value);
} else if (formatter instanceof TimeFormatter) {
this.formatValue = (value: any) => formatter(value);
} else {
this.formatValue = formatter;
}

const scale = extractScale<Output>(definition, options.namespace);
this.scale = scale;
if (typeof scale === 'undefined') {
this.encodeValue = identity;
} else if (scale instanceof CategoricalColorScale) {
this.encodeValue = (value: any) => scale(`${value}`);
} else {
this.encodeValue = (value: any) => scale(value);
}

this.axis = extractAxis(name, definition, this.formatter);
}

get(datum: PlainObject, otherwise?: any) {
const value = this.getValue(datum);

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

encode(datum: PlainObject, otherwise?: Output) {
const output = this.encodeValue(this.get(datum));

return otherwise !== undefined && (output === null || output === undefined)
? otherwise
: output;
}

format(datum: PlainObject): string {
return this.formatValue(this.get(datum));
}

hasLegend() {
if (isDisabled(this.options.legend)) {
return false;
}
if (isXYChannel(this.name)) {
return false;
}
if (isValueDef(this.definition)) {
return false;
}
if (isMarkPropFieldDef(this.definition)) {
return isEnabled(this.definition.legend);
}

return isScaleFieldDef(this.definition);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { cloneDeep } from 'lodash';
import { Axis } from 'vega-lite/build/src/axis';
import { ChannelDef, isPositionFieldDef, Formatter } from '../types/FieldDef';
import extractFormat from './extractFormat';
import { PlainObject } from '../types/Data';

export function isXYChannel(channelName: string) {
return channelName === 'x' || channelName === 'y';
}

function isAxis(axis: Axis | null | undefined | false): axis is Axis {
return axis !== false && axis !== null && axis !== undefined;
}

export default function extractAxis(
channelName: string,
definition: ChannelDef,
defaultFormatter: Formatter,
) {
if (isXYChannel(channelName) && isPositionFieldDef(definition)) {
const { type, axis } = definition;
if (isAxis(axis)) {
const parsedAxis: PlainObject = cloneDeep(axis);
const { labels } = parsedAxis;
const { format } = labels;
parsedAxis.format = format
? extractFormat({ field: definition.field, format: axis.format, type })
: defaultFormatter;

return parsedAxis;
}
}

return undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getNumberFormatter } from '@superset-ui/number-format';
import { getTimeFormatter } from '@superset-ui/time-format';
import { isTypedFieldDef, ChannelDef } from '../types/FieldDef';

export default function extractFormat(definition: ChannelDef) {
if (isTypedFieldDef(definition)) {
const { type } = definition;
const format =
'format' in definition && definition.format !== undefined ? definition.format : '';
switch (type) {
case 'quantitative':
return getNumberFormatter(format);
case 'temporal':
return getTimeFormatter(format);
default:
}
}

return (v: any) => `${v}`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { get } from 'lodash/fp';
import { isValueDef, ChannelDef } from '../types/FieldDef';
import identity from '../utils/identity';

export default function extractGetter(definition: ChannelDef) {
if (isValueDef(definition)) {
return () => definition.value;
} else if ('field' in definition && definition.field !== undefined) {
return get(definition.field);
}

return identity;
}
Loading

0 comments on commit 3f68efb

Please sign in to comment.