This repository has been archived by the owner on Jun 25, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add encodeable utilities for chart (#15)
* 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
Showing
19 changed files
with
556 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | ||
}; | ||
} | ||
} |
98 changes: 98 additions & 0 deletions
98
packages/superset-ui-preset-chart-xy/src/encodeable/AbstractEncoder.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
100 changes: 100 additions & 0 deletions
100
packages/superset-ui-preset-chart-xy/src/encodeable/ChannelEncoder.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
packages/superset-ui-preset-chart-xy/src/encodeable/parsers/extractAxis.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
20 changes: 20 additions & 0 deletions
20
packages/superset-ui-preset-chart-xy/src/encodeable/parsers/extractFormat.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`; | ||
} |
13 changes: 13 additions & 0 deletions
13
packages/superset-ui-preset-chart-xy/src/encodeable/parsers/extractGetter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.