Skip to content

Commit

Permalink
feat: add box plot (#78)
Browse files Browse the repository at this point in the history
Squashed commits:
[23ad0d6] feat: working box plot
[a7ed565] fix: typings
[57a62b7] feat: clarify horizontal/vertical mode
[6312737] fix: typings
[2734de3] feat: box plot types
[cb3c239] fix: typings
[7e2dcda] feat: add box plot
  • Loading branch information
kristw authored and zhaoyongjie committed Nov 26, 2021
1 parent eefb1cf commit 6c7533b
Show file tree
Hide file tree
Showing 21 changed files with 738 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const BOX_PLOT_PLUGIN_TYPE = 'v2-box-plot';
export const BOX_PLOT_PLUGIN_LEGACY_TYPE = 'v2-box-plot/legacy';
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable sort-keys, no-magic-numbers */
export default [
{
label: 'East Asia & Pacific',
values: {
Q1: 1384725172.5,
Q2: 1717904169.0,
Q3: 2032724922.5,
whisker_high: 2240687901.0,
whisker_low: 1031863394.0,
outliers: [],
},
},
{
label: 'Europe & Central Asia',
values: {
Q1: 751386460.5,
Q2: 820716895.0,
Q3: 862814192.5,
whisker_high: 903095786.0,
whisker_low: 660881033.0,
outliers: [],
},
},
{
label: 'Latin America & Caribbean',
values: {
Q1: 313690832.5,
Q2: 421490233.0,
Q3: 529668114.5,
whisker_high: 626270167.0,
whisker_low: 220564224.0,
outliers: [],
},
},
{
label: 'Middle East & North Africa',
values: {
Q1: 152382756.5,
Q2: 232066828.0,
Q3: 318191071.5,
whisker_high: 417451428.0,
whisker_low: 105512645.0,
outliers: [],
},
},
{
label: 'North America',
values: {
Q1: 235506847.5,
Q2: 268896849.0,
Q3: 314553651.5,
whisker_high: 354462656.0,
whisker_low: 198624409.0,
outliers: [],
},
},
{
label: 'South Asia',
values: {
Q1: 772373036.5,
Q2: 1059570231.0,
Q3: 1398841234.0,
whisker_high: 1720976995.0,
whisker_low: 572036107.0,
outliers: [],
},
},
{
label: 'Sub-Saharan Africa',
values: {
Q1: 320037758.0,
Q2: 467337821.0,
Q3: 676768689.0,
whisker_high: 974315323.0,
whisker_low: 228268752.0,
outliers: [],
},
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BoxPlotChartPlugin as LegacyBoxPlotChartPlugin } from '../../../../../superset-ui-preset-chart-xy/src/legacy';
import { BoxPlotChartPlugin } from '../../../../../superset-ui-preset-chart-xy/src';
import Stories from './stories/Basic';
import LegacyStories from './stories/Legacy';
import { BOX_PLOT_PLUGIN_LEGACY_TYPE, BOX_PLOT_PLUGIN_TYPE } from './constants';

new LegacyBoxPlotChartPlugin().configure({ key: BOX_PLOT_PLUGIN_LEGACY_TYPE }).register();
new BoxPlotChartPlugin().configure({ key: BOX_PLOT_PLUGIN_TYPE }).register();

export default {
examples: [...Stories, ...LegacyStories],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* eslint-disable no-magic-numbers, sort-keys */
import React from 'react';
import { SuperChart, ChartProps } from '@superset-ui/chart';
import data from '../data';

export default [
{
renderStory: () => (
<SuperChart
chartType="v2-box-plot"
chartProps={
new ChartProps({
datasource: { verboseMap: {} },
formData: {
encoding: {
x: {
type: 'nominal',
field: 'label',
scale: {
type: 'band',
paddingInner: 0.15,
paddingOuter: 0.3,
},
axis: {
label: 'Region',
},
},
y: {
field: 'value',
type: 'quantitative',
scale: {
type: 'linear',
},
axis: {
label: 'Population',
numTicks: 5,
},
},
color: {
type: 'nominal',
field: 'label',
scale: {
scheme: 'd3Category10',
},
},
},
},
height: 400,
payload: { data },
width: 400,
})
}
/>
),
storyName: 'Basic',
storyPath: 'preset-chart-xy|BoxPlotChartPlugin',
},
{
renderStory: () => (
<SuperChart
chartType="v2-box-plot"
chartProps={
new ChartProps({
datasource: { verboseMap: {} },
formData: {
encoding: {
y: {
type: 'nominal',
field: 'label',
scale: {
type: 'band',
paddingInner: 0.15,
paddingOuter: 0.3,
},
axis: {
label: 'Region',
},
},
x: {
field: 'value',
type: 'quantitative',
scale: {
type: 'linear',
},
axis: {
label: 'Population',
numTicks: 5,
},
},
color: {
type: 'nominal',
field: 'label',
scale: {
scheme: 'd3Category10',
},
},
},
},
height: 400,
payload: { data },
width: 400,
})
}
/>
),
storyName: 'Horizontal',
storyPath: 'preset-chart-xy|BoxPlotChartPlugin',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* eslint-disable no-magic-numbers, sort-keys */
import React from 'react';
import { SuperChart, ChartProps } from '@superset-ui/chart';
import data from '../data';

export default [
{
renderStory: () => (
<SuperChart
chartType="v2-box-plot/legacy"
chartProps={
new ChartProps({
datasource: { verboseMap: {} },
formData: {
colorScheme: 'd3Category10',
groupby: ['region'],
metrics: ['sum__SP_POP_TOTL'],
vizType: 'box_plot',
whiskerOptions: 'Min/max (no outliers)',
},
height: 400,
payload: { data },
width: 400,
})
}
/>
),
storyName: 'Use Legacy API shim',
storyPath: 'preset-chart-xy|BoxPlotChartPlugin',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable sort-keys, no-magic-numbers, complexity */
import React from 'react';
import { BoxPlotSeries, XYChart } from '@data-ui/xy-chart';
import { chartTheme, ChartTheme } from '@data-ui/theme';
import { Margin, Dimension } from '@superset-ui/dimension';
import { createSelector } from 'reselect';
import createTooltip from './createTooltip';
import XYChartLayout from '../utils/XYChartLayout';
import WithLegend from '../components/WithLegend';
import ChartLegend from '../components/ChartLegend';
import Encoder, { ChannelTypes, Encoding, Outputs } from './Encoder';
import { Dataset, PlainObject } from '../encodeable/types/Data';

chartTheme.gridStyles.stroke = '#f1f3f5';

const DEFAULT_MARGIN = { top: 20, right: 20, left: 20, bottom: 20 };

const defaultProps = {
className: '',
margin: DEFAULT_MARGIN,
theme: chartTheme,
} as const;

type Props = {
className?: string;
width: string | number;
height: string | number;
margin?: Margin;
encoding: Encoding;
data: Dataset;
theme?: ChartTheme;
} & Readonly<typeof defaultProps>;

export default class BoxPlot extends React.PureComponent<Props> {
static defaultProps = defaultProps;

constructor(props: Props) {
super(props);

const createEncoder = createSelector(
(enc: Encoding) => enc,
(enc: Encoding) => new Encoder({ encoding: enc }),
);

this.createEncoder = () => {
this.encoder = createEncoder(this.props.encoding);
};

this.encoder = createEncoder(this.props.encoding);
this.renderChart = this.renderChart.bind(this);
}

encoder: Encoder;
private createEncoder: () => void;

renderChart(dim: Dimension) {
const { width, height } = dim;
const { data, encoding, margin, theme } = this.props;
const { channels } = this.encoder;

const isHorizontal = encoding.y.type === 'nominal';

const children = [
<BoxPlotSeries
key={channels.x.definition.field}
animated
data={
isHorizontal
? data.map(row => ({ ...row, y: channels.y.get(row) }))
: data.map(row => ({ ...row, x: channels.x.get(row) }))
}
fill={(datum: PlainObject) => channels.color.encode(datum, '#55acee')}
fillOpacity={0.4}
stroke={(datum: PlainObject) => channels.color.encode(datum)}
strokeWidth={1}
widthRatio={0.6}
horizontal={encoding.y.type === 'nominal'}
/>,
];

const layout = new XYChartLayout({
width,
height,
margin: { ...DEFAULT_MARGIN, ...margin },
theme,
xEncoder: channels.x,
yEncoder: channels.y,
children,
});

return layout.renderChartWithFrame((chartDim: Dimension) => (
<XYChart
width={chartDim.width}
height={chartDim.height}
ariaLabel="BoxPlot"
margin={layout.margin}
renderTooltip={createTooltip(this.encoder)}
showYGrid
theme={theme}
xScale={channels.x.definition.scale}
yScale={channels.y.definition.scale}
>
{children}
{layout.renderXAxis()}
{layout.renderYAxis()}
</XYChart>
));
}

render() {
const { className, data, width, height } = this.props;

this.createEncoder();
const renderLegend = this.encoder.hasLegend()
? // eslint-disable-next-line react/jsx-props-no-multi-spaces
() => <ChartLegend<ChannelTypes, Outputs, Encoding> data={data} encoder={this.encoder} />
: undefined;

return (
<WithLegend
className={`superset-chart-box-plot ${className}`}
width={width}
height={height}
position="top"
renderLegend={renderLegend}
renderChart={this.renderChart}
/>
);
}
}

0 comments on commit 6c7533b

Please sign in to comment.