Skip to content
This repository has been archived by the owner on Jun 25, 2020. It is now read-only.

Commit

Permalink
fix: legend types
Browse files Browse the repository at this point in the history
  • Loading branch information
kristw committed Jun 5, 2019
1 parent dd7c414 commit b704671
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 82 deletions.
2 changes: 1 addition & 1 deletion packages/superset-ui-preset-chart-xy/src/Line/Encoder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Value } from 'vega-lite/build/src/channeldef';
import AbstractEncoder from '../encodeable/AbstractEncoder';
import { PartialSpec } from '../encodeable/types/Specification';
import { ChannelTypeToDefMap, ChannelType } from '../encodeable/types/Channel';
import { ChannelTypeToDefMap } from '../encodeable/types/Channel';
import { ExtractChannelOutput } from '../encodeable/types/ChannelDef';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/superset-ui-preset-chart-xy/src/Line/Line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export type FormDataProps = {
} & PartialSpec<Encoding>;

export type HookProps = {
LegendRenderer?: React.ComponentType<LegendProps<Encoder, ChannelTypes>>;
LegendRenderer?: React.ComponentType<LegendProps<Encoder>>;
TooltipRenderer?: React.ComponentType<TooltipProps>;
} & LegendHooks<ChannelTypes>;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import React, { CSSProperties, PureComponent } from 'react';
import AbstractEncoder from '../../encodeable/AbstractEncoder';
import { Dataset } from '../../encodeable/types/Data';
import { ChannelType } from '../../encodeable/types/Channel';
import {
LegendItemRendererType,
LegendGroupRendererType,
LegendItemLabelRendererType,
LegendItemMarkRendererType,
} from './types';
import DefaultLegendGroup from './DefaultLegendGroup';
import { ChannelDef } from '../../encodeable/types/ChannelDef';

const LEGEND_CONTAINER_STYLE: CSSProperties = {
display: 'flex',
Expand All @@ -21,24 +19,22 @@ const LEGEND_CONTAINER_STYLE: CSSProperties = {
position: 'relative',
};

export type Hooks<ChannelTypes> = {
LegendGroupRenderer?: LegendGroupRendererType<ChannelTypes>;
LegendItemRenderer?: LegendItemRendererType<ChannelTypes>;
LegendItemMarkRenderer?: LegendItemMarkRendererType<ChannelTypes>;
LegendItemLabelRenderer?: LegendItemLabelRendererType<ChannelTypes>;
export type Hooks<Encoding> = {
LegendGroupRenderer?: LegendGroupRendererType<Encoding>;
LegendItemRenderer?: LegendItemRendererType<Encoding>;
LegendItemMarkRenderer?: LegendItemMarkRendererType<Encoding>;
LegendItemLabelRenderer?: LegendItemLabelRendererType<Encoding>;
};

export type Props<ChannelTypes, Encoder> = {
export type Props<Encoder> = {
data: Dataset;
encoder: Encoder;
style?: CSSProperties;
} & Hooks<ChannelTypes>;
} & Hooks<Encoder extends AbstractEncoder<any, infer Encoding> ? Encoding : never>;

export default class ChartLegend<
Encoder extends AbstractEncoder<ChannelTypes, Encoding>,
ChannelTypes extends Record<string, ChannelType> = any,
Encoding extends Record<keyof ChannelTypes, ChannelDef> = any
> extends PureComponent<Props<ChannelTypes, Encoder>, {}> {
export default class ChartLegend<Encoder extends AbstractEncoder<any, any>> extends PureComponent<
Props<Encoder>
> {
render() {
const {
data,
Expand Down
39 changes: 19 additions & 20 deletions packages/superset-ui-preset-chart-xy/src/components/legend/types.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,39 @@
import { Value } from 'vega-lite/build/src/channeldef';
import { CSSProperties } from 'react';
import { ObjectWithKeysFromAndValueType } from '../../encodeable/types/Base';
import { ChannelInput } from '../../encodeable/types/Channel';

export type LegendItemInfo<ChannelTypes> = {
export type LegendItemInfo<Encoding> = {
field: string;
value: ChannelInput;
encodedValues: Partial<ObjectWithKeysFromAndValueType<ChannelTypes, Value | undefined>>;
encodedValues: Partial<Record<keyof Encoding, Value | undefined>>;
};

export type LegendItemMarkRendererType<ChannelTypes> = React.ComponentType<{
item: LegendItemInfo<ChannelTypes>;
export type LegendItemMarkRendererType<Encoding> = React.ComponentType<{
item: LegendItemInfo<Encoding>;
}>;

export type LegendItemLabelRendererType<ChannelTypes> = React.ComponentType<{
item: LegendItemInfo<ChannelTypes>;
export type LegendItemLabelRendererType<Encoding> = React.ComponentType<{
item: LegendItemInfo<Encoding>;
}>;

export type LegendItemRendererProps<ChannelTypes> = {
item: LegendItemInfo<ChannelTypes>;
MarkRenderer?: LegendItemMarkRendererType<ChannelTypes>;
LabelRenderer?: LegendItemLabelRendererType<ChannelTypes>;
export type LegendItemRendererProps<Encoding> = {
item: LegendItemInfo<Encoding>;
MarkRenderer?: LegendItemMarkRendererType<Encoding>;
LabelRenderer?: LegendItemLabelRendererType<Encoding>;
};

export type LegendItemRendererType<ChannelTypes> = React.ComponentType<
LegendItemRendererProps<ChannelTypes>
export type LegendItemRendererType<Encoding> = React.ComponentType<
LegendItemRendererProps<Encoding>
>;

export type LegendGroupRendererProps<ChannelTypes> = {
items: LegendItemInfo<ChannelTypes>[];
ItemRenderer?: LegendItemRendererType<ChannelTypes>;
ItemMarkRenderer?: LegendItemMarkRendererType<ChannelTypes>;
ItemLabelRenderer?: LegendItemLabelRendererType<ChannelTypes>;
export type LegendGroupRendererProps<Encoding> = {
items: LegendItemInfo<Encoding>[];
ItemRenderer?: LegendItemRendererType<Encoding>;
ItemMarkRenderer?: LegendItemMarkRendererType<Encoding>;
ItemLabelRenderer?: LegendItemLabelRendererType<Encoding>;
style?: CSSProperties;
};

export type LegendGroupRendererType<ChannelTypes> = React.ComponentType<
LegendGroupRendererProps<ChannelTypes>
export type LegendGroupRendererType<Encoding> = React.ComponentType<
LegendGroupRendererProps<Encoding>
>;
Original file line number Diff line number Diff line change
@@ -1,78 +1,97 @@
import { flatMap } from 'lodash';
import { Value } from 'vega-lite/build/src/channeldef';
import { ObjectWithKeysFromAndValueType } from './types/Base';
import { ChannelOptions, ChannelType, ChannelInput } from './types/Channel';
import { FullSpec, BaseOptions, PartialSpec } from './types/Specification';
import { isFieldDef, isTypedFieldDef, FieldDef, ChannelDef } from './types/ChannelDef';
import ChannelEncoder from './ChannelEncoder';
import { Dataset } from './types/Data';
import { Unarray, MayBeArray, isArray, isNotArray } from './types/Base';

export default abstract class AbstractEncoder<
ChannelTypes extends Record<string, ChannelType>,
Encoding extends Record<keyof ChannelTypes, ChannelDef>,
Encoding extends Record<keyof ChannelTypes, MayBeArray<ChannelDef>>,
Options extends BaseOptions = BaseOptions
> {
readonly channelTypes: ChannelTypes;
readonly spec: FullSpec<Encoding, Options>;
readonly channels: { readonly [k in keyof ChannelTypes]: ChannelEncoder<Encoding[k]> };
readonly channels: {
readonly [k in keyof Encoding]: Encoding[k] extends any[]
? ChannelEncoder<Unarray<Encoding[k]>>[]
: ChannelEncoder<Unarray<Encoding[k]>>
};

readonly commonChannels: {
group: ChannelEncoder<FieldDef>[];
tooltip: ChannelEncoder<FieldDef>[];
};

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

constructor(
channelTypes: ChannelTypes,
spec: PartialSpec<Encoding, Options>,
defaultEncoding?: Encoding,
channelOptions: Partial<{ [k in keyof ChannelTypes]: ChannelOptions }> = {},
channelOptions: Partial<{ [k in keyof Encoding]: ChannelOptions }> = {},
) {
this.channelTypes = channelTypes;
this.spec = this.createFullSpec(spec, defaultEncoding);

type ChannelName = keyof ChannelTypes;
type Channels = { readonly [k in ChannelName]: ChannelEncoder<Encoding[k]> };

const channelNames = Object.keys(this.channelTypes) as ChannelName[];

const { encoding } = this.spec;
this.channels = channelNames
.map(
(name: ChannelName) =>
new ChannelEncoder<Encoding[typeof name]>({
definition: encoding[name],
name,
options: {
...this.spec.options,
...channelOptions[name],
},
type: channelTypes[name],
}),
)
.reduce((prev: Partial<Channels>, curr) => {
const all = prev;
all[curr.name as ChannelName] = curr;

return all;
}, {}) as Channels;
type ChannelName = keyof Encoding;
type Channels = {
readonly [k in keyof Encoding]: Encoding[k] extends any[]
? ChannelEncoder<Unarray<Encoding[k]>>[]
: ChannelEncoder<Unarray<Encoding[k]>>
};

const channelNames = this.getChannelNames();

const tmp: { [k in keyof Encoding]?: MayBeArray<ChannelEncoder<ChannelDef>> } = {};

channelNames.forEach(name => {
const channelEncoding = encoding[name];
if (isArray(channelEncoding)) {
const definitions = channelEncoding;
tmp[name] = definitions.map(
(definition, i) =>
new ChannelEncoder({
definition,
name: `${name}[${i}]`,
type: 'Text',
}),
);
} else if (isNotArray(channelEncoding)) {
const definition = channelEncoding;
tmp[name] = new ChannelEncoder({
definition,
name,
options: {
...this.spec.options,
...channelOptions[name],
},
type: channelTypes[name],
});
}
});

this.channels = tmp as Channels;

this.commonChannels = {
group: this.spec.commonEncoding.group.map(
(def, i) =>
new ChannelEncoder({
definition: def,
name: `group${i}`,
name: `group[${i}]`,
type: 'Text',
}),
),
tooltip: this.spec.commonEncoding.tooltip.map(
(def, i) =>
new ChannelEncoder({
definition: def,
name: `tooltip${i}`,
name: `tooltip[${i}]`,
type: 'Text',
}),
),
Expand All @@ -83,9 +102,8 @@ export default abstract class AbstractEncoder<
this.legends = {};
channelNames
.map((name: ChannelName) => this.channels[name])
.filter(c => c.hasLegend())
.forEach(c => {
if (isFieldDef(c.definition)) {
if (isNotArray(c) && c.hasLegend() && isFieldDef(c.definition)) {
const name = c.name as ChannelName;
const { field } = c.definition;
if (this.legends[field]) {
Expand Down Expand Up @@ -130,7 +148,7 @@ export default abstract class AbstractEncoder<
}

getGroupBys() {
const fields = this.getChannelsAsArray()
const fields = flatMap(this.getChannelsAsArray())
.filter(c => c.isGroupBy())
.map(c => (isFieldDef(c.definition) ? c.definition.field : ''))
.filter(field => field !== '');
Expand All @@ -144,7 +162,7 @@ export default abstract class AbstractEncoder<
const channelNames = this.legends[field];
const channelEncoder = this.channels[channelNames[0]];

if (isTypedFieldDef(channelEncoder.definition)) {
if (isNotArray(channelEncoder) && isTypedFieldDef(channelEncoder.definition)) {
// Only work for nominal channels now
// TODO: Add support for numerical scale
if (channelEncoder.definition.type === 'nominal') {
Expand All @@ -155,12 +173,12 @@ export default abstract class AbstractEncoder<
value,
// eslint-disable-next-line sort-keys
encodedValues: channelNames.reduce(
(
prev: Partial<ObjectWithKeysFromAndValueType<ChannelTypes, Value | undefined>>,
curr,
) => {
(prev: Partial<Record<keyof Encoding, Value | undefined>>, curr) => {
const map = prev;
map[curr] = this.channels[curr].encodeValue(value);
const channel = this.channels[curr];
if (isNotArray(channel)) {
map[curr] = channel.encodeValue(value);
}

return map;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable no-magic-numbers */
import { CSSProperties } from 'react';
import { Value } from 'vega-lite/build/src/channeldef';
import { getTextDimension, Margin, Dimension } from '@superset-ui/dimension';
import { CategoricalColorScale } from '@superset-ui/color';
import { extractFormatFromTypeAndFormat } from './parsers/extractFormat';
Expand Down
11 changes: 11 additions & 0 deletions packages/superset-ui-preset-chart-xy/src/encodeable/types/Base.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
// eslint-disable-next-line import/prefer-default-export
export type ObjectWithKeysFromAndValueType<T extends {}, V> = { [key in keyof T]: V };

export type Unarray<T> = T extends Array<infer U> ? U : T;
export type MayBeArray<T> = T | T[];

export function isArray<T>(maybeArray: T | T[]): maybeArray is T[] {
return Array.isArray(maybeArray);
}

export function isNotArray<T>(maybeArray: T | T[]): maybeArray is T {
return !Array.isArray(maybeArray);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Value } from 'vega-lite/build/src/channeldef';
import { XFieldDef, YFieldDef, ChannelDef, MarkPropChannelDef, TextChannelDef } from './ChannelDef';
import { ObjectWithKeysFromAndValueType } from './Base';
import { XFieldDef, YFieldDef, MarkPropChannelDef, TextChannelDef } from './ChannelDef';

export type ChannelInput = number | string | boolean | null | Date | undefined;

Expand All @@ -13,8 +12,7 @@ export interface ChannelOptions {
/**
* Define all channel types and mapping to available definition grammar
*/
export interface ChannelTypeToDefMap<Output extends Value = Value>
extends ObjectWithKeysFromAndValueType<ChannelTypeToDefMap<Output>, ChannelDef> {
export interface ChannelTypeToDefMap<Output extends Value = Value> {
// position on x-axis
X: XFieldDef<Output>;
// position on y-axis
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export type NonValueDef<Output extends Value = Value> =

export type ChannelDef<Output extends Value = Value> = NonValueDef<Output> | ValueDef<Output>;

export type ExtractChannelOutput<C> = C extends ChannelDef<infer Output> ? Output : never;
export type ExtractChannelOutput<Def> = Def extends ChannelDef<infer Output> ? Output : never;

export function isValueDef<Output extends Value>(
channelDef: ChannelDef<Output>,
Expand Down

0 comments on commit b704671

Please sign in to comment.