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

Commit d3cc3f9

Browse files
authored
feat: migrate xy-chart to use encodable (#420)
* feat: use encodable in BoxPlot * feat: migrate legend * docs: update storybook * fix: label overlap * fix: lint * fix: remove comments * fix: path
1 parent 806bba2 commit d3cc3f9

20 files changed

+736
-167
lines changed

packages/superset-ui-preset-chart-xy/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"csstype": "^2.6.3",
4242
"d3-array": "^2.1.0",
4343
"d3-scale": "^3.0.0",
44+
"encodable": "^0.2.6",
4445
"lodash": "^4.17.11",
4546
"prop-types": "^15.6.2",
4647
"reselect": "^4.0.0",

packages/superset-ui-preset-chart-xy/src/BoxPlot/BoxPlot.tsx

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,74 +4,67 @@ import { chartTheme, ChartTheme } from '@data-ui/theme';
44
import { Margin, Dimension } from '@superset-ui/dimension';
55
import { WithLegend } from '@superset-ui/chart-composition';
66
import DefaultTooltipRenderer from './DefaultTooltipRenderer';
7-
import Encoder, { Encoding } from './Encoder';
7+
import {
8+
BoxPlotEncodingConfig,
9+
BoxPlotEncoding,
10+
BoxPlotEncoder,
11+
boxPlotEncoderFactory,
12+
} from './Encoder';
813
import { Dataset, PlainObject } from '../encodeable/types/Data';
9-
import { PartialSpec } from '../encodeable/types/Specification';
10-
import createMarginSelector, { DEFAULT_MARGIN } from '../utils/selectors/createMarginSelector';
14+
import createMarginSelector, { DEFAULT_MARGIN } from '../utils2/createMarginSelector';
1115
import { BoxPlotDataRow } from './types';
1216
import convertScaleToDataUIScale from '../utils/convertScaleToDataUIScaleShape';
13-
import createXYChartLayoutWithTheme from '../utils/createXYChartLayoutWithTheme';
14-
import createEncoderSelector from '../encodeable/createEncoderSelector';
15-
import createRenderLegend from '../components/legend/createRenderLegend';
16-
import { LegendHooks } from '../components/legend/types';
17+
import createXYChartLayoutWithTheme from '../utils2/createXYChartLayoutWithTheme';
18+
import createRenderLegend from '../components/legend2/createRenderLegend';
19+
import { LegendHooks } from '../components/legend2/types';
20+
import { isFieldDef } from '../encodeable/types/ChannelDef';
1721

1822
export interface TooltipProps {
1923
datum: BoxPlotDataRow;
2024
color: string;
21-
encoder: Encoder;
25+
encoder: BoxPlotEncoder;
2226
}
2327

2428
const defaultProps = {
2529
className: '',
2630
margin: DEFAULT_MARGIN,
31+
encoding: {},
2732
theme: chartTheme,
2833
TooltipRenderer: DefaultTooltipRenderer,
2934
} as const;
3035

3136
export type HookProps = {
3237
TooltipRenderer?: React.ComponentType<TooltipProps>;
33-
} & LegendHooks<Encoder>;
38+
} & LegendHooks<BoxPlotEncodingConfig>;
3439

3540
type Props = {
3641
className?: string;
3742
width: string | number;
3843
height: string | number;
3944
margin?: Margin;
45+
encoding?: Partial<BoxPlotEncoding>;
4046
data: Dataset;
4147
theme?: ChartTheme;
42-
} & PartialSpec<Encoding> &
43-
HookProps &
48+
} & HookProps &
4449
Readonly<typeof defaultProps>;
4550

4651
export default class BoxPlot extends React.PureComponent<Props> {
47-
private createEncoder = createEncoderSelector(Encoder);
52+
private createEncoder = boxPlotEncoderFactory.createSelector();
4853

4954
private createMargin = createMarginSelector();
5055

5156
static defaultProps = defaultProps;
5257

53-
constructor(props: Props) {
54-
super(props);
55-
56-
this.renderChart = this.renderChart.bind(this);
57-
}
58-
59-
renderChart(dim: Dimension) {
58+
renderChart = (dim: Dimension) => {
6059
const { width, height } = dim;
61-
const { data, margin, theme, TooltipRenderer } = this.props;
62-
const encoder = this.createEncoder(this.props);
60+
const { data, margin, theme, TooltipRenderer, encoding } = this.props;
61+
const encoder = this.createEncoder(encoding);
6362
const { channels } = encoder;
6463

65-
const isHorizontal = channels.y.definition.type === 'nominal';
64+
const isHorizontal =
65+
isFieldDef(channels.y.definition) && channels.y.definition.type === 'nominal';
6666

67-
if (typeof channels.x.scale !== 'undefined') {
68-
const xDomain = channels.x.getDomain(data);
69-
channels.x.scale.setDomain(xDomain);
70-
}
71-
if (typeof channels.y.scale !== 'undefined') {
72-
const yDomain = channels.y.getDomain(data);
73-
channels.y.scale.setDomain(yDomain);
74-
}
67+
encoder.setDomainFromDataset(data);
7568

7669
const layout = createXYChartLayoutWithTheme({
7770
width,
@@ -93,34 +86,34 @@ export default class BoxPlot extends React.PureComponent<Props> {
9386
<TooltipRenderer datum={datum} color={color} encoder={encoder} />
9487
)}
9588
theme={theme}
96-
xScale={convertScaleToDataUIScale(channels.x.scale!.config)}
97-
yScale={convertScaleToDataUIScale(channels.y.scale!.config)}
89+
xScale={convertScaleToDataUIScale(channels.x.definition.scale as any)}
90+
yScale={convertScaleToDataUIScale(channels.y.definition.scale as any)}
9891
>
9992
{layout.renderXAxis()}
10093
{layout.renderYAxis()}
10194
<BoxPlotSeries
102-
key={channels.x.definition.field}
95+
key={isFieldDef(channels.x.definition) ? channels.x.definition.field : ''}
10396
animated
10497
data={
10598
isHorizontal
106-
? data.map(row => ({ ...row, y: channels.y.get(row) }))
107-
: data.map(row => ({ ...row, x: channels.x.get(row) }))
99+
? data.map(row => ({ ...row, y: channels.y.getValueFromDatum(row) }))
100+
: data.map(row => ({ ...row, x: channels.x.getValueFromDatum(row) }))
108101
}
109-
fill={(datum: PlainObject) => channels.color.encode(datum, '#55acee')}
102+
fill={(datum: PlainObject) => channels.color.encodeDatum(datum, '#55acee')}
110103
fillOpacity={0.4}
111-
stroke={(datum: PlainObject) => channels.color.encode(datum)}
104+
stroke={(datum: PlainObject) => channels.color.encodeDatum(datum)}
112105
strokeWidth={1}
113106
widthRatio={0.6}
114-
horizontal={channels.y.definition.type === 'nominal'}
107+
horizontal={isHorizontal}
115108
/>
116109
</XYChart>
117110
));
118-
}
111+
};
119112

120113
render() {
121-
const { className, data, width, height } = this.props;
114+
const { className, data, encoding, width, height } = this.props;
122115

123-
const encoder = this.createEncoder(this.props);
116+
const encoder = this.createEncoder(encoding);
124117

125118
return (
126119
<WithLegend

packages/superset-ui-preset-chart-xy/src/BoxPlot/DefaultTooltipRenderer.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { isDefined } from '@superset-ui/core';
33
import { TooltipFrame, TooltipTable } from '@superset-ui/chart-composition';
4-
import Encoder from './Encoder';
4+
import { BoxPlotEncoder } from './Encoder';
55
import { BoxPlotDataRow } from './types';
66

77
export default function DefaultTooltipRenderer({
@@ -11,13 +11,12 @@ export default function DefaultTooltipRenderer({
1111
}: {
1212
datum: BoxPlotDataRow;
1313
color: string;
14-
encoder: Encoder;
14+
encoder: BoxPlotEncoder;
1515
}) {
1616
const { label, min, max, median, firstQuartile, thirdQuartile, outliers } = datum;
1717
const { channels } = encoder;
1818

19-
const formatValue =
20-
channels.y.definition.type === 'nominal' ? channels.x.formatValue : channels.y.formatValue;
19+
const { formatValue } = channels.y;
2120

2221
const data = [];
2322
if (isDefined(min)) {
Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,26 @@
1-
import { Value } from 'vega-lite/build/src/channeldef';
2-
import { ChannelTypeToDefMap } from '../encodeable/types/Channel';
3-
import { ExtractChannelOutput } from '../encodeable/types/ChannelDef';
4-
import createEncoderClass from '../encodeable/createEncoderClass';
1+
import { createEncoderFactory, Encoder } from 'encodable';
2+
import { DeriveEncoding } from 'encodable/lib/types/Encoding';
53

6-
/**
7-
* Define channel types
8-
*/
9-
const channelTypes = {
10-
color: 'Color',
11-
x: 'XBand',
12-
y: 'YBand',
13-
} as const;
14-
15-
export type ChannelTypes = typeof channelTypes;
16-
17-
/**
18-
* TEMPLATE:
19-
* Helper for defining encoding
20-
*/
21-
type CreateChannelDef<
22-
ChannelName extends keyof ChannelTypes,
23-
Output extends Value
24-
> = ChannelTypeToDefMap<Output>[ChannelTypes[ChannelName]];
25-
26-
/**
27-
* Encoding definition
28-
*/
29-
export type Encoding = {
30-
color: CreateChannelDef<'color', string>;
31-
x: CreateChannelDef<'x', number | null>;
32-
y: CreateChannelDef<'y', number | null>;
4+
export type BoxPlotEncodingConfig = {
5+
x: ['XBand', number];
6+
y: ['YBand', number];
7+
color: ['Color', string];
338
};
349

35-
/**
36-
* TEMPLATE:
37-
* Can use this to get returned type of a Channel
38-
* example usage: ChannelOutput<'x'>
39-
*/
40-
export type ChannelOutput<ChannelName extends keyof Encoding> = ExtractChannelOutput<
41-
Encoding[ChannelName]
42-
>;
43-
44-
export default class Encoder extends createEncoderClass<ChannelTypes, Encoding>({
45-
channelTypes,
10+
// eslint-disable-next-line import/prefer-default-export
11+
export const boxPlotEncoderFactory = createEncoderFactory<BoxPlotEncodingConfig>({
12+
channelTypes: {
13+
x: 'XBand',
14+
y: 'YBand',
15+
color: 'Color',
16+
},
4617
defaultEncoding: {
47-
color: { value: '#222' },
4818
x: { field: 'x', type: 'nominal' },
4919
y: { field: 'y', type: 'quantitative' },
20+
color: { value: '#222' },
5021
},
51-
}) {}
22+
});
23+
24+
export type BoxPlotEncoding = DeriveEncoding<BoxPlotEncodingConfig>;
25+
26+
export type BoxPlotEncoder = Encoder<BoxPlotEncodingConfig>;

packages/superset-ui-preset-chart-xy/src/Line/Line.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Encoder, { Encoding, ChannelOutput } from './Encoder';
1616
import { Dataset, PlainObject } from '../encodeable/types/Data';
1717
import { PartialSpec } from '../encodeable/types/Specification';
1818
import DefaultTooltipRenderer from './DefaultTooltipRenderer';
19-
import createMarginSelector, { DEFAULT_MARGIN } from '../utils/selectors/createMarginSelector';
19+
import createMarginSelector, { DEFAULT_MARGIN } from '../utils2/createMarginSelector';
2020
import convertScaleToDataUIScale from '../utils/convertScaleToDataUIScaleShape';
2121
import createXYChartLayoutWithTheme from '../utils/createXYChartLayoutWithTheme';
2222
import createEncoderSelector from '../encodeable/createEncoderSelector';

packages/superset-ui-preset-chart-xy/src/ScatterPlot/ScatterPlot.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { WithLegend } from '@superset-ui/chart-composition';
66
import Encoder, { Encoding, ChannelOutput } from './Encoder';
77
import { Dataset, PlainObject } from '../encodeable/types/Data';
88
import { PartialSpec } from '../encodeable/types/Specification';
9-
import createMarginSelector, { DEFAULT_MARGIN } from '../utils/selectors/createMarginSelector';
9+
import createMarginSelector, { DEFAULT_MARGIN } from '../utils2/createMarginSelector';
1010
import DefaultTooltipRenderer from './DefaultTooltipRenderer';
1111
import convertScaleToDataUIScale from '../utils/convertScaleToDataUIScaleShape';
1212
import { isScaleFieldDef } from '../encodeable/types/ChannelDef';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { CSSProperties, PureComponent } from 'react';
2+
import { EncodingConfig } from 'encodable/lib/types/Encoding';
3+
import { LegendRendererProps } from './types';
4+
import DefaultLegendGroup from './DefaultLegendGroup';
5+
6+
const LEGEND_CONTAINER_STYLE: CSSProperties = {
7+
display: 'flex',
8+
flexBasis: 'auto',
9+
flexGrow: 1,
10+
flexShrink: 1,
11+
maxHeight: 100,
12+
overflowY: 'auto',
13+
position: 'relative',
14+
};
15+
16+
export type Props<Config extends EncodingConfig> = LegendRendererProps<Config>;
17+
18+
export default class DefaultLegend<Config extends EncodingConfig> extends PureComponent<
19+
Props<Config>
20+
> {
21+
render() {
22+
const {
23+
groups,
24+
LegendGroupRenderer = DefaultLegendGroup,
25+
LegendItemRenderer,
26+
LegendItemMarkRenderer,
27+
LegendItemLabelRenderer,
28+
style,
29+
} = this.props;
30+
31+
const combinedStyle =
32+
typeof style === 'undefined'
33+
? LEGEND_CONTAINER_STYLE
34+
: { ...LEGEND_CONTAINER_STYLE, ...style };
35+
36+
return (
37+
<div style={combinedStyle}>
38+
{groups
39+
.filter(group => 'items' in group && group.items.length > 0)
40+
.map(group => (
41+
<LegendGroupRenderer
42+
key={group.field}
43+
group={group}
44+
ItemRenderer={LegendItemRenderer}
45+
ItemMarkRenderer={LegendItemMarkRenderer}
46+
ItemLabelRenderer={LegendItemLabelRenderer}
47+
/>
48+
))}
49+
</div>
50+
);
51+
}
52+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React, { CSSProperties } from 'react';
2+
import { EncodingConfig } from 'encodable/lib/types/Encoding';
3+
import { LegendGroupRendererProps } from './types';
4+
import DefaultLegendItem from './DefaultLegendItem';
5+
6+
const LEGEND_GROUP_STYLE: CSSProperties = {
7+
display: 'flex',
8+
flexBasis: 'auto',
9+
flexDirection: 'row',
10+
flexGrow: 1,
11+
flexShrink: 1,
12+
flexWrap: 'wrap',
13+
fontSize: '0.8em',
14+
justifyContent: 'flex-end',
15+
padding: 8,
16+
};
17+
18+
export default function DefaultLegendGroupRenderer<Config extends EncodingConfig>({
19+
group,
20+
ItemRenderer = DefaultLegendItem,
21+
ItemMarkRenderer,
22+
ItemLabelRenderer,
23+
style,
24+
}: LegendGroupRendererProps<Config>) {
25+
const combinedStyle =
26+
typeof style === 'undefined' ? LEGEND_GROUP_STYLE : { ...LEGEND_GROUP_STYLE, ...style };
27+
28+
return (
29+
<div style={combinedStyle}>
30+
{'items' in group &&
31+
group.items.map(item => (
32+
<ItemRenderer
33+
key={`legend-item-${group.field}-${item.input}`}
34+
group={group}
35+
item={item}
36+
MarkRenderer={ItemMarkRenderer}
37+
LabelRenderer={ItemLabelRenderer}
38+
/>
39+
))}
40+
</div>
41+
);
42+
}

0 commit comments

Comments
 (0)