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

feat: Add Encoder #230

Merged
merged 8 commits into from
Oct 4, 2019
Merged

feat: Add Encoder #230

merged 8 commits into from
Oct 4, 2019

Conversation

kristw
Copy link
Contributor

@kristw kristw commented Oct 2, 2019

馃弳 Enhancements

Add Encoder to @superset-ui/encodable. An Encoder is a utility class created once per chart type.

  • It consists of one or more ChannelEncoder
  • It takes users' definitions of the channels (vega-lite syntax), then resolves ambiguity and produce complete definition and actionable utility functions.

Example Usage

The component developer creates an encoder factory once.

type LineChartEncodingConfig = {
  // channelName: [ChannelType, output type, support multiple fields or not]
  x: ['X', number];
  y: ['Y', number];
  color: ['Color', string];
  tooltip: ['Text', string, 'multiple'];
};

const lineChartEncoderFactory = createEncoderFactory<LineChartEncodingConfig>({
    // This part is manual work to convert the `ChannelType` defined in `LineChartEncodingConfig` above
    // from `type` to `value`. `LineChartEncodingConfig` is the source of truth 
    // and because it is generic for this function, type checking will ensure the only compatible value
    // for channelTypes is exactly like the one below.
    channelTypes: {
      x: 'X',
      y: 'Y',
      color: 'Color',
      tooltip: 'Text',
    },
    // Define default definition for each channel
    defaultEncoding: {
      x: { type: 'quantitative', field: 'x' },
      y: { type: 'quantitative', field: 'y' },
      color: { value: 'black' },
      tooltip: [],
    },
  });

type LineChartEncoding = DeriveEncoding<LineChartEncodingConfig>;

The factory encapsulates the awkward channelTypes and defaultEncoding

  • which are constants across all Encoder instance of this chart
  • making it convenient to create a new Encoder from encoding because factory.create(encoding) only needs one argument: encoding.

This is how user-specified encoding may look like.

const encoding: LineChartEncoding = {
  x: { type: 'quantitative', field: 'speed', scale: { domain: [0, 100] } },
  y: { type: 'quantitative', field: 'price' },
  color: { type: 'nominal', field: 'brand' },
  tooltip: [{ field: 'make' }, { field: 'model' }],
};

Then create an Encoder for the incoming encoding and use it to facilitate rendering.

const encoder = lineChartEncoderFactory.create(encoding);
encoder.channels.x.getValueFromDatum({ speed: 100 }); // 100
encoder.channels.x.encodeDatum({ speed: 100 }); // 1
encoder.getGroupBys(); // ['brand', 'make', 'model']

@kristw kristw requested a review from a team as a code owner October 2, 2019 08:04
@netlify
Copy link

netlify bot commented Oct 2, 2019

Deploy preview for superset-ui ready!

Built with commit 34960fd

https://deploy-preview-230--superset-ui.netlify.com

@kristw kristw changed the title [WIP] feat: Add Encoder feat: Add Encoder Oct 2, 2019
@codecov
Copy link

codecov bot commented Oct 2, 2019

Codecov Report

Merging #230 into master will not change coverage.
The diff coverage is 100%.

Impacted file tree graph

@@          Coverage Diff          @@
##           master   #230   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files         138    142    +4     
  Lines        1520   1559   +39     
  Branches      400    406    +6     
=====================================
+ Hits         1520   1559   +39
Impacted Files Coverage 螖
...set-ui-encodable/src/fillers/completeChannelDef.ts 100% <酶> (酶) 猬嗭笍
...ages/superset-ui-encodable/src/encoders/Encoder.ts 100% <100%> (酶)
...t-ui-encodable/src/fillers/completeLegendConfig.ts 100% <100%> (酶)
...perset-ui-encodable/src/encoders/ChannelEncoder.ts 100% <100%> (酶) 猬嗭笍
...-ui-encodable/src/encoders/createEncoderFactory.ts 100% <100%> (酶)
...s/superset-ui-encodable/src/utils/mergeEncoding.ts 100% <100%> (酶)

Continue to review full report at Codecov.

Legend - Click here to learn more
螖 = absolute <relative> (impact), 酶 = not affected, ? = missing data
Powered by Codecov. Last update e7d7249...34960fd. Read the comment docs.

Copy link
Contributor

@williaster williaster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, had a couples questions. very cool types!

the vx re-write has made me more appreciative of TS and nuances :)

channels[name] = definitions.map(
(definition, i) =>
new ChannelEncoder({
channelType: 'Text',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this come from channelTypes[name] or channelTypes[name][0]? or Text is the only type that has an array? (seems possibly fragile in the future)

channelNames
.map(name => this.channels[name])
.forEach(c => {
if (isNotArray(c) && c.hasLegend() && isFieldDef(c.definition)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we not care about channels encoded as arrays in legends?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, the channels that are array are fields for tooltip. I tried to think if we want to support other channels that require legends such as color as array of multiple fields but that makes thing very complicated. Better create a derived field field1,field2 in the dataset and use that derived field instead. (vega also has support for formula instead of field for this kind of purpose, which I haven't explored how to port)

@@ -126,4 +126,8 @@ export default class ChannelEncoder<Def extends ChannelDef<Output>, Output exten
isY() {
return isY(this.channelType);
}

hasLegend() {
return this.definition.legend !== false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since it looks like legend is optional, should this check if it's set to true?

EDIT: I see that the complete encoding specifies false or { ... }.

import { EncodingConfig, DeriveChannelTypes, DeriveEncoding } from '../types/Encoding';
import mergeEncoding from '../utils/mergeEncoding';

type CreateEncoderFactoryParams<Config extends EncodingConfig> = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this type is beautiful 馃樆

};

export type DeriveChannelTypes<Config extends EncodingConfig> = {
readonly [k in keyof Config]: Config[k]['0'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#TIL, did not know that you could index types like this.

interesting it has to be a string and not a number.


export type DeriveChannelEncoders<Config extends EncodingConfig> = {
readonly [k in keyof Config]: Config[k]['2'] extends 'multiple'
? ChannelEncoder<ChannelTypeToDefMap<Config[k]['1']>[Config[k]['0']]>[]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so advanced! 馃く

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is like 3rd-4th time I rewrote the whole thing lol.

@kristw kristw merged commit 03aa6b3 into master Oct 4, 2019
@delete-merged-branch delete-merged-branch bot deleted the kristw--encoder branch October 4, 2019 22:16
kristw pushed a commit that referenced this pull request Apr 17, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants