refactor(monorepo): move superset-ui to superset(stage 2) (#17552)

This commit is contained in:
Yongjie Zhao
2021-11-30 08:29:57 +08:00
committed by GitHub
parent bfba4f1689
commit 3c41ff68a4
1315 changed files with 27755 additions and 15167 deletions

View File

@@ -0,0 +1,91 @@
/**
* 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.
*/
import React, { useCallback } from 'react';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';
import { BoxPlotChartTransformedProps } from './types';
export default function EchartsBoxPlot({
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
}: BoxPlotChartTransformedProps) {
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
return;
}
const groupbyValues = values.map(value => labelMap[value]);
setDataMask({
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL',
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
});
},
[groupby, labelMap, setDataMask, selectedValues],
);
const eventHandlers: EventHandlers = {
click: props => {
const { name } = props;
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([name]);
}
},
};
return (
<Echart
height={height}
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
selectedValues={selectedValues}
/>
);
}

View File

@@ -0,0 +1,41 @@
/**
* 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.
*/
import { buildQueryContext } from '@superset-ui/core';
import { boxplotOperator } from '@superset-ui/chart-controls';
import { BoxPlotQueryFormData } from './types';
export default function buildQuery(formData: BoxPlotQueryFormData) {
const { columns = [], granularity_sqla, groupby = [] } = formData;
return buildQueryContext(formData, baseQueryObject => {
const distributionColumns: string[] = [];
// For now default to using the temporal column as distribution column.
// In the future this control should be made mandatory.
if (!columns.length && granularity_sqla) {
distributionColumns.push(granularity_sqla);
}
return [
{
...baseQueryObject,
columns: [...distributionColumns, ...columns, ...groupby],
series_columns: groupby,
post_processing: [boxplotOperator(formData, baseQueryObject)],
},
];
});
}

View File

@@ -0,0 +1,140 @@
/**
* 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.
*/
import { t } from '@superset-ui/core';
import {
D3_FORMAT_DOCS,
D3_FORMAT_OPTIONS,
D3_TIME_FORMAT_OPTIONS,
formatSelectOptions,
sections,
emitFilterControl,
ControlPanelConfig,
} from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [
['metrics'],
['adhoc_filters'],
emitFilterControl,
['groupby'],
['columns'], // TODO: this should be migrated to `series_columns`
['series_limit'],
['series_limit_metric'],
[
{
name: 'whiskerOptions',
config: {
clearable: false,
type: 'SelectControl',
freeForm: true,
label: t('Whisker/outlier options'),
default: 'Tukey',
description: t(
'Determines how whiskers and outliers are calculated.',
),
choices: formatSelectOptions([
'Tukey',
'Min/max (no outliers)',
'2/98 percentiles',
'9/91 percentiles',
]),
},
},
],
],
},
sections.titleControls,
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
[
{
name: 'x_ticks_layout',
config: {
type: 'SelectControl',
label: t('X Tick Layout'),
choices: formatSelectOptions([
'auto',
'flat',
'45°',
'90°',
'staggered',
]),
default: 'auto',
clearable: false,
renderTrigger: true,
description: t('The way the ticks are laid out on the X-axis'),
},
},
],
[
{
name: 'number_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Number format'),
renderTrigger: true,
default: 'SMART_NUMBER',
choices: D3_FORMAT_OPTIONS,
description: `${t(
'D3 format syntax: https://github.com/d3/d3-format',
)} ${t('Only applies when "Label Type" is set to show values.')}`,
},
},
],
[
{
name: 'date_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Date format'),
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
default: 'smart_date',
description: D3_FORMAT_DOCS,
},
},
],
],
},
],
controlOverrides: {
groupby: {
label: t('Series'),
description: t('Categories to group by on the x-axis.'),
},
columns: {
label: t('Distribute across'),
multi: true,
description: t(
'Columns to calculate distribution across. Defaults to temporal column if left empty.',
),
},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,61 @@
/**
* 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.
*/
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import example from './images/BoxPlot.jpg';
import thumbnail from './images/thumbnail.png';
import { BoxPlotQueryFormData, EchartsBoxPlotChartProps } from './types';
export default class EchartsBoxPlotChartPlugin extends ChartPlugin<
BoxPlotQueryFormData,
EchartsBoxPlotChartProps
> {
/**
* The constructor is used to pass relevant metadata and callbacks that get
* registered in respective registries that are used throughout the library
* and application. A more thorough description of each property is given in
* the respective imported file.
*
* It is worth noting that `buildQuery` and is optional, and only needed for
* advanced visualizations that require either post processing operations
* (pivoting, rolling aggregations, sorting etc) or submitting multiple queries.
*/
constructor() {
super({
buildQuery,
controlPanel,
loadChart: () => import('./EchartsBoxPlot'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART],
category: t('Distribution'),
credits: ['https://echarts.apache.org'],
description: t(
'Also known as a box and whisker plot, this visualization compares the distributions of a related metric across multiple groups. The box in the middle emphasizes the mean, median, and inner 2 quartiles. The whiskers around each box visualize the min, max, range, and outer 2 quartiles.',
),
exampleGallery: [{ url: example }],
name: t('Box Plot'),
tags: [t('ECharts'), t('Range'), t('Statistical')],
thumbnail,
}),
transformProps,
});
}
}

View File

@@ -0,0 +1,288 @@
/**
* 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.
*/
import {
CategoricalColorNamespace,
DataRecordValue,
getColumnLabel,
getMetricLabel,
getNumberFormatter,
getTimeFormatter,
} from '@superset-ui/core';
import { EChartsCoreOption, BoxplotSeriesOption } from 'echarts';
import { CallbackDataParams } from 'echarts/types/src/util/types';
import {
BoxPlotChartTransformedProps,
BoxPlotQueryFormData,
EchartsBoxPlotChartProps,
} from './types';
import {
extractGroupbyLabel,
getColtypesMapping,
sanitizeHtml,
} from '../utils/series';
import { defaultGrid, defaultTooltip, defaultYAxis } from '../defaults';
import { getPadding } from '../Timeseries/transformers';
import { OpacityEnum } from '../constants';
export default function transformProps(
chartProps: EchartsBoxPlotChartProps,
): BoxPlotChartTransformedProps {
const { width, height, formData, hooks, filterState, queriesData } =
chartProps;
const { data = [] } = queriesData[0];
const { setDataMask = () => {} } = hooks;
const coltypeMapping = getColtypesMapping(queriesData[0]);
const {
colorScheme,
groupby = [],
metrics = [],
numberFormat,
dateFormat,
xTicksLayout,
emitFilter,
legendOrientation = 'top',
xAxisTitle,
yAxisTitle,
xAxisTitleMargin,
yAxisTitleMargin,
yAxisTitlePosition,
} = formData as BoxPlotQueryFormData;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getNumberFormatter(numberFormat);
const metricLabels = metrics.map(getMetricLabel);
const groupbyLabels = groupby.map(getColumnLabel);
const transformedData = data
.map((datum: any) => {
const groupbyLabel = extractGroupbyLabel({
datum,
groupby: groupbyLabels,
coltypeMapping,
timeFormatter: getTimeFormatter(dateFormat),
});
return metricLabels.map(metric => {
const name =
metricLabels.length === 1
? groupbyLabel
: `${groupbyLabel}, ${metric}`;
const isFiltered =
filterState.selectedValues &&
!filterState.selectedValues.includes(name);
return {
name,
value: [
datum[`${metric}__min`],
datum[`${metric}__q1`],
datum[`${metric}__median`],
datum[`${metric}__q3`],
datum[`${metric}__max`],
datum[`${metric}__mean`],
datum[`${metric}__count`],
datum[`${metric}__outliers`],
],
itemStyle: {
color: colorFn(groupbyLabel),
opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6,
borderColor: colorFn(groupbyLabel),
},
};
});
})
.flatMap(row => row);
const outlierData = data
.map(datum =>
metricLabels.map(metric => {
const groupbyLabel = extractGroupbyLabel({
datum,
groupby: groupbyLabels,
coltypeMapping,
timeFormatter: getTimeFormatter(dateFormat),
});
const name =
metricLabels.length === 1
? groupbyLabel
: `${groupbyLabel}, ${metric}`;
// Outlier data is a nested array of numbers (uncommon, therefore no need to add to DataRecordValue)
const outlierDatum = (datum[`${metric}__outliers`] || []) as number[];
const isFiltered =
filterState.selectedValues &&
!filterState.selectedValues.includes(name);
return {
name: 'outlier',
type: 'scatter',
data: outlierDatum.map(val => [name, val]),
tooltip: {
formatter: (param: { data: [string, number] }) => {
const [outlierName, stats] = param.data;
const headline = groupbyLabels.length
? `<p><strong>${sanitizeHtml(outlierName)}</strong></p>`
: '';
return `${headline}${numberFormatter(stats)}`;
},
},
itemStyle: {
color: colorFn(groupbyLabel),
opacity: isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent,
},
};
}),
)
.flat(2);
const labelMap = data.reduce(
(acc: Record<string, DataRecordValue[]>, datum) => {
const label = extractGroupbyLabel({
datum,
groupby: groupbyLabels,
coltypeMapping,
timeFormatter: getTimeFormatter(dateFormat),
});
return {
...acc,
[label]: groupbyLabels.map(col => datum[col]),
};
},
{},
);
const selectedValues = (filterState.selectedValues || []).reduce(
(acc: Record<string, number>, selectedValue: string) => {
const index = transformedData.findIndex(
({ name }) => name === selectedValue,
);
return {
...acc,
[index]: selectedValue,
};
},
{},
);
let axisLabel;
if (xTicksLayout === '45°') axisLabel = { rotate: -45 };
else if (xTicksLayout === '90°') axisLabel = { rotate: -90 };
else if (xTicksLayout === 'flat') axisLabel = { rotate: 0 };
else if (xTicksLayout === 'staggered') axisLabel = { rotate: -45 };
else axisLabel = { show: true };
const series: BoxplotSeriesOption[] = [
{
name: 'boxplot',
type: 'boxplot',
data: transformedData,
tooltip: {
formatter: (param: CallbackDataParams) => {
// @ts-ignore
const {
value,
name,
}: {
value: [
number,
number,
number,
number,
number,
number,
number,
number,
number[],
];
name: string;
} = param;
const headline = name
? `<p><strong>${sanitizeHtml(name)}</strong></p>`
: '';
const stats = [
`Max: ${numberFormatter(value[5])}`,
`3rd Quartile: ${numberFormatter(value[4])}`,
`Mean: ${numberFormatter(value[6])}`,
`Median: ${numberFormatter(value[3])}`,
`1st Quartile: ${numberFormatter(value[2])}`,
`Min: ${numberFormatter(value[1])}`,
`# Observations: ${numberFormatter(value[7])}`,
];
if (value[8].length > 0) {
stats.push(`# Outliers: ${numberFormatter(value[8].length)}`);
}
return headline + stats.join('<br/>');
},
},
},
// @ts-ignore
...outlierData,
];
const addYAxisTitleOffset = !!yAxisTitle;
const addXAxisTitleOffset = !!xAxisTitle;
const chartPadding = getPadding(
true,
legendOrientation,
addYAxisTitleOffset,
false,
null,
addXAxisTitleOffset,
yAxisTitlePosition,
yAxisTitleMargin,
xAxisTitleMargin,
);
const echartOptions: EChartsCoreOption = {
grid: {
...defaultGrid,
...chartPadding,
},
xAxis: {
type: 'category',
data: transformedData.map(row => row.name),
axisLabel,
name: xAxisTitle,
nameGap: xAxisTitleMargin,
nameLocation: 'middle',
},
yAxis: {
...defaultYAxis,
type: 'value',
axisLabel: { formatter: numberFormatter },
name: yAxisTitle,
nameGap: yAxisTitleMargin,
nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
},
tooltip: {
...defaultTooltip,
trigger: 'item',
axisPointer: {
type: 'shadow',
},
},
series,
};
return {
formData,
width,
height,
echartOptions,
setDataMask,
emitFilter,
labelMap,
groupby,
selectedValues,
};
}

View File

@@ -0,0 +1,71 @@
/**
* 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.
*/
import {
ChartDataResponseResult,
ChartProps,
DataRecordValue,
QueryFormColumn,
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import { EChartsCoreOption } from 'echarts';
import { EchartsTitleFormData, DEFAULT_TITLE_FORM_DATA } from '../types';
export type BoxPlotQueryFormData = QueryFormData & {
numberFormat?: string;
whiskerOptions?: BoxPlotFormDataWhiskerOptions;
xTickLayout?: BoxPlotFormXTickLayout;
emitFilter: boolean;
} & EchartsTitleFormData;
export type BoxPlotFormDataWhiskerOptions =
| 'Tukey'
| 'Min/max (no outliers)'
| '2/98 percentiles'
| '9/91 percentiles';
export type BoxPlotFormXTickLayout =
| '45°'
| '90°'
| 'auto'
| 'flat'
| 'staggered';
// @ts-ignore
export const DEFAULT_FORM_DATA: BoxPlotQueryFormData = {
emitFilter: false,
...DEFAULT_TITLE_FORM_DATA,
};
export interface EchartsBoxPlotChartProps extends ChartProps {
formData: BoxPlotQueryFormData;
queriesData: ChartDataResponseResult[];
}
export interface BoxPlotChartTransformedProps {
formData: BoxPlotQueryFormData;
height: number;
width: number;
echartOptions: EChartsCoreOption;
emitFilter: boolean;
setDataMask: SetDataMaskHook;
labelMap: Record<string, DataRecordValue[]>;
groupby: QueryFormColumn[];
selectedValues: Record<number, string>;
}