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,29 @@
/**
* 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 from 'react';
import { EchartsProps } from '../types';
import Echart from '../components/Echart';
export default function EchartsGraph({
height,
width,
echartOptions,
}: EchartsProps) {
return <Echart height={height} width={width} echartOptions={echartOptions} />;
}

View File

@@ -0,0 +1,30 @@
/**
* 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, QueryFormData } from '@superset-ui/core';
export default function buildQuery(formData: QueryFormData) {
return buildQueryContext(formData, {
queryFields: {
source: 'columns',
target: 'columns',
source_category: 'columns',
target_category: 'columns',
},
});
}

View File

@@ -0,0 +1,57 @@
/**
* 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 { GraphSeriesOption } from 'echarts';
export const DEFAULT_GRAPH_SERIES_OPTION: GraphSeriesOption = {
zoom: 0.7,
circular: { rotateLabel: true },
force: {
initLayout: 'circular',
layoutAnimation: true,
},
label: {
show: true,
position: 'right',
distance: 5,
rotate: 0,
offset: [0, 0],
fontStyle: 'normal',
fontWeight: 'normal',
fontFamily: 'sans-serif',
fontSize: 12,
padding: [0, 0, 0, 0],
overflow: 'truncate',
formatter: '{b}',
},
emphasis: {
focus: 'adjacency',
},
animation: true,
animationDuration: 500,
animationEasing: 'cubicOut',
lineStyle: { color: 'source', curveness: 0.1 },
select: {
itemStyle: { borderWidth: 3, opacity: 1 },
label: { fontWeight: 'bolder' },
},
// Ref: https://echarts.apache.org/en/option.html#series-graph.data.tooltip.formatter
// - b: data name
// - c: data value
tooltip: { formatter: '{b}: {c}' },
};

View File

@@ -0,0 +1,325 @@
/**
* 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 from 'react';
import { t } from '@superset-ui/core';
import {
ControlPanelConfig,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
import { legendSection } from '../controls';
const requiredEntity = {
...sharedControls.entity,
clearable: false,
};
const optionalEntity = {
...sharedControls.entity,
clearable: true,
validators: [],
};
const controlPanel: ControlPanelConfig = {
controlPanelSections: [
sections.legacyRegularTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [
[
{
name: 'source',
config: {
...requiredEntity,
label: t('Source'),
description: t('Name of the source nodes'),
},
},
],
[
{
name: 'target',
config: {
...requiredEntity,
label: t('Target'),
description: t('Name of the target nodes'),
},
},
],
['metric'],
[
{
name: 'source_category',
config: {
...optionalEntity,
label: t('Source category'),
description: t(
'The category of source nodes used to assign colors. ' +
'If a node is associated with more than one category, only the first will be used.',
),
},
},
],
[
{
name: 'target_category',
config: {
...optionalEntity,
label: t('Target category'),
description: t('Category of target nodes'),
},
},
],
['adhoc_filters'],
['row_limit'],
],
},
{
label: t('Chart options'),
expanded: true,
controlSetRows: [
['color_scheme'],
...legendSection,
[<h1 className="section-header">{t('Layout')}</h1>],
[
{
name: 'layout',
config: {
type: 'RadioButtonControl',
renderTrigger: true,
label: t('Graph layout'),
default: DEFAULT_FORM_DATA.layout,
options: [
['force', t('Force')],
['circular', t('Circular')],
],
description: t('Layout type of graph'),
},
},
],
[
{
name: 'edgeSymbol',
config: {
type: 'SelectControl',
renderTrigger: true,
label: t('Edge symbols'),
description: t('Symbol of two ends of edge line'),
default: DEFAULT_FORM_DATA.edgeSymbol,
choices: [
['none,none', t('None -> None')],
['none,arrow', t('None -> Arrow')],
['circle,arrow', t('Circle -> Arrow')],
['circle,circle', t('Circle -> Circle')],
],
},
},
],
[
{
name: 'draggable',
config: {
type: 'CheckboxControl',
label: t('Enable node dragging'),
renderTrigger: true,
default: DEFAULT_FORM_DATA.draggable,
description: t(
'Whether to enable node dragging in force layout mode.',
),
visibility({ form_data: { layout } }) {
return (
layout === 'force' ||
(!layout && DEFAULT_FORM_DATA.layout === 'force')
);
},
},
},
],
[
{
name: 'roam',
config: {
type: 'SelectControl',
label: t('Enable graph roaming'),
renderTrigger: true,
default: DEFAULT_FORM_DATA.roam,
choices: [
[false, t('Disabled')],
['scale', t('Scale only')],
['move', t('Move only')],
[true, t('Scale and Move')],
],
description: t(
'Whether to enable changing graph position and scaling.',
),
},
},
],
[
{
name: 'selectedMode',
config: {
type: 'SelectControl',
renderTrigger: true,
label: t('Node select mode'),
default: DEFAULT_FORM_DATA.selectedMode,
choices: [
[false, t('Disabled')],
['single', t('Single')],
['multiple', t('Multiple')],
],
description: t('Allow node selections'),
},
},
],
[
{
name: 'showSymbolThreshold',
config: {
type: 'TextControl',
label: t('Label threshold'),
renderTrigger: true,
isInt: true,
default: DEFAULT_FORM_DATA.showSymbolThreshold,
description: t(
'Minimum value for label to be displayed on graph.',
),
},
},
],
[
{
name: 'baseNodeSize',
config: {
type: 'TextControl',
label: t('Node size'),
renderTrigger: true,
isFloat: true,
default: DEFAULT_FORM_DATA.baseNodeSize,
description: t(
'Median node size, the largest node will be 4 times larger than the smallest',
),
},
},
{
name: 'baseEdgeWidth',
config: {
type: 'TextControl',
label: t('Edge width'),
renderTrigger: true,
isFloat: true,
default: DEFAULT_FORM_DATA.baseEdgeWidth,
description: t(
'Median edge width, the thickest edge will be 4 times thicker than the thinnest.',
),
},
},
],
[
{
name: 'edgeLength',
config: {
type: 'SliderControl',
label: t('Edge length'),
renderTrigger: true,
min: 100,
max: 1000,
step: 50,
default: DEFAULT_FORM_DATA.edgeLength,
description: t('Edge length between nodes'),
visibility({ form_data: { layout } }) {
return (
layout === 'force' ||
(!layout && DEFAULT_FORM_DATA.layout === 'force')
);
},
},
},
],
[
{
name: 'gravity',
config: {
type: 'SliderControl',
label: t('Gravity'),
renderTrigger: true,
min: 0.1,
max: 1,
step: 0.1,
default: DEFAULT_FORM_DATA.gravity,
description: t('Strength to pull the graph toward center'),
visibility({ form_data: { layout } }) {
return (
layout === 'force' ||
(!layout && DEFAULT_FORM_DATA.layout === 'force')
);
},
},
},
],
[
{
name: 'repulsion',
config: {
type: 'SliderControl',
label: t('Repulsion'),
renderTrigger: true,
min: 100,
max: 3000,
step: 50,
default: DEFAULT_FORM_DATA.repulsion,
description: t('Repulsion strength between nodes'),
visibility({ form_data: { layout } }) {
return (
layout === 'force' ||
(!layout && DEFAULT_FORM_DATA.layout === 'force')
);
},
},
},
],
[
{
name: 'friction',
config: {
type: 'SliderControl',
label: t('Friction'),
renderTrigger: true,
min: 0.1,
max: 1,
step: 0.1,
default: DEFAULT_FORM_DATA.friction,
description: t('Friction between nodes'),
visibility({ form_data: { layout } }) {
return (
layout === 'force' ||
(!layout && DEFAULT_FORM_DATA.layout === 'force')
);
},
},
},
],
],
},
],
};
export default controlPanel;

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1,53 @@
/**
* 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 } from '@superset-ui/core';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import buildQuery from './buildQuery';
export default class EchartsGraphChartPlugin extends ChartPlugin {
constructor() {
super({
buildQuery,
controlPanel,
loadChart: () => import('./EchartsGraph'),
metadata: new ChartMetadata({
category: t('Flow'),
credits: ['https://echarts.apache.org'],
description: t(
'Displays connections between entities in a graph structure. Useful for mapping relationships and showing which nodes are important in a network. Graph charts can be configured to be force-directed or circulate. If your data has a geospatial component, try the deck.gl Arc chart.',
),
name: t('Graph Chart'),
tags: [
t('Aesthetic'),
t('Circular'),
t('Comparison'),
t('Directional'),
t('ECharts'),
t('Relational'),
t('Structural'),
t('Transformable'),
],
thumbnail,
}),
transformProps,
});
}
}

View File

@@ -0,0 +1,316 @@
/**
* 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,
ChartProps,
getMetricLabel,
DataRecord,
DataRecordValue,
} from '@superset-ui/core';
import { EChartsCoreOption, GraphSeriesOption } from 'echarts';
import { extent as d3Extent } from 'd3-array';
import { GraphEdgeItemOption } from 'echarts/types/src/chart/graph/GraphSeries';
import {
EchartsGraphFormData,
EChartGraphNode,
DEFAULT_FORM_DATA as DEFAULT_GRAPH_FORM_DATA,
EdgeSymbol,
} from './types';
import { DEFAULT_GRAPH_SERIES_OPTION } from './constants';
import { EchartsProps } from '../types';
import { getChartPadding, getLegendProps, sanitizeHtml } from '../utils/series';
type EdgeWithStyles = GraphEdgeItemOption & {
lineStyle: Exclude<GraphEdgeItemOption['lineStyle'], undefined>;
emphasis: Exclude<GraphEdgeItemOption['emphasis'], undefined>;
select: Exclude<GraphEdgeItemOption['select'], undefined>;
};
function verifyEdgeSymbol(symbol: string): EdgeSymbol {
if (symbol === 'none' || symbol === 'circle' || symbol === 'arrow') {
return symbol;
}
return 'none';
}
function parseEdgeSymbol(symbols?: string | null): [EdgeSymbol, EdgeSymbol] {
const [start, end] = (symbols || '').split(',');
return [verifyEdgeSymbol(start), verifyEdgeSymbol(end)];
}
/**
* Emphasized edge width with a min and max.
*/
function getEmphasizedEdgeWidth(width: number) {
return Math.max(5, Math.min(width * 2, 20));
}
/**
* Normalize node size, edge width, and apply label visibility thresholds.
*/
function normalizeStyles(
nodes: EChartGraphNode[],
links: EdgeWithStyles[],
{
baseNodeSize,
baseEdgeWidth,
showSymbolThreshold,
}: {
baseNodeSize: number;
baseEdgeWidth: number;
showSymbolThreshold?: number;
},
) {
const minNodeSize = baseNodeSize * 0.5;
const maxNodeSize = baseNodeSize * 2;
const minEdgeWidth = baseEdgeWidth * 0.5;
const maxEdgeWidth = baseEdgeWidth * 2;
const [nodeMinValue, nodeMaxValue] = d3Extent(nodes, x => x.value) as [
number,
number,
];
const nodeSpread = nodeMaxValue - nodeMinValue;
nodes.forEach(node => {
// eslint-disable-next-line no-param-reassign
node.symbolSize =
(((node.value - nodeMinValue) / nodeSpread) * maxNodeSize || 0) +
minNodeSize;
// eslint-disable-next-line no-param-reassign
node.label = {
...node.label,
show: showSymbolThreshold ? node.value > showSymbolThreshold : true,
};
});
const [linkMinValue, linkMaxValue] = d3Extent(links, x => x.value) as [
number,
number,
];
const linkSpread = linkMaxValue - linkMinValue;
links.forEach(link => {
const lineWidth =
((link.value! - linkMinValue) / linkSpread) * maxEdgeWidth ||
0 + minEdgeWidth;
// eslint-disable-next-line no-param-reassign
link.lineStyle.width = lineWidth;
// eslint-disable-next-line no-param-reassign
link.emphasis.lineStyle = {
...link.emphasis.lineStyle,
width: getEmphasizedEdgeWidth(lineWidth),
};
// eslint-disable-next-line no-param-reassign
link.select.lineStyle = {
...link.select.lineStyle,
width: getEmphasizedEdgeWidth(lineWidth * 0.8),
opacity: 1,
};
});
}
function getKeyByValue(
object: { [name: string]: number },
value: number,
): string {
return Object.keys(object).find(key => object[key] === value) as string;
}
function edgeFormatter(
sourceIndex: string,
targetIndex: string,
value: number,
nodes: { [name: string]: number },
): string {
const source = Number(sourceIndex);
const target = Number(targetIndex);
return `${sanitizeHtml(getKeyByValue(nodes, source))} > ${sanitizeHtml(
getKeyByValue(nodes, target),
)} : ${value}`;
}
function getCategoryName(columnName: string, name?: DataRecordValue) {
if (name === false) {
return `${columnName}: false`;
}
if (name === true) {
return `${columnName}: true`;
}
if (name == null) {
return 'N/A';
}
return String(name);
}
export default function transformProps(chartProps: ChartProps): EchartsProps {
const { width, height, formData, queriesData } = chartProps;
const data: DataRecord[] = queriesData[0].data || [];
const {
source,
target,
sourceCategory,
targetCategory,
colorScheme,
metric = '',
layout,
roam,
draggable,
selectedMode,
showSymbolThreshold,
edgeLength,
gravity,
repulsion,
friction,
legendMargin,
legendOrientation,
legendType,
showLegend,
baseEdgeWidth,
baseNodeSize,
edgeSymbol,
}: EchartsGraphFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData };
const metricLabel = getMetricLabel(metric);
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const nodes: { [name: string]: number } = {};
const categories: Set<string> = new Set();
const echartNodes: EChartGraphNode[] = [];
const echartLinks: EdgeWithStyles[] = [];
/**
* Get the node id of an existing node,
* or create a new node if it doesn't exist.
*/
function getOrCreateNode(name: string, category?: string) {
if (!(name in nodes)) {
nodes[name] = echartNodes.length;
echartNodes.push({
id: String(nodes[name]),
name,
value: 0,
category,
select: DEFAULT_GRAPH_SERIES_OPTION.select,
tooltip: DEFAULT_GRAPH_SERIES_OPTION.tooltip,
});
}
const node = echartNodes[nodes[name]];
if (category) {
categories.add(category);
// category may be empty when one of `sourceCategory`
// or `targetCategory` is not set.
if (!node.category) {
node.category = category;
}
}
return node;
}
data.forEach(link => {
const value = link[metricLabel] as number;
if (!value) {
return;
}
const sourceName = link[source] as string;
const targetName = link[target] as string;
const sourceCategoryName = sourceCategory
? getCategoryName(sourceCategory, link[sourceCategory])
: undefined;
const targetCategoryName = targetCategory
? getCategoryName(targetCategory, link[targetCategory])
: undefined;
const sourceNode = getOrCreateNode(sourceName, sourceCategoryName);
const targetNode = getOrCreateNode(targetName, targetCategoryName);
sourceNode.value += value;
targetNode.value += value;
echartLinks.push({
source: sourceNode.id,
target: targetNode.id,
value,
lineStyle: {},
emphasis: {},
select: {},
});
});
normalizeStyles(echartNodes, echartLinks, {
showSymbolThreshold,
baseEdgeWidth,
baseNodeSize,
});
const categoryList = [...categories];
const series: GraphSeriesOption[] = [
{
zoom: DEFAULT_GRAPH_SERIES_OPTION.zoom,
type: 'graph',
categories: categoryList.map(c => ({
name: c,
itemStyle: { color: colorFn(c) },
})),
layout,
force: {
...DEFAULT_GRAPH_SERIES_OPTION.force,
edgeLength,
gravity,
repulsion,
friction,
},
circular: DEFAULT_GRAPH_SERIES_OPTION.circular,
data: echartNodes,
links: echartLinks,
roam,
draggable,
edgeSymbol: parseEdgeSymbol(edgeSymbol),
edgeSymbolSize: baseEdgeWidth * 2,
selectedMode,
...getChartPadding(showLegend, legendOrientation, legendMargin),
animation: DEFAULT_GRAPH_SERIES_OPTION.animation,
label: DEFAULT_GRAPH_SERIES_OPTION.label,
lineStyle: DEFAULT_GRAPH_SERIES_OPTION.lineStyle,
emphasis: DEFAULT_GRAPH_SERIES_OPTION.emphasis,
},
];
const echartOptions: EChartsCoreOption = {
animationDuration: DEFAULT_GRAPH_SERIES_OPTION.animationDuration,
animationEasing: DEFAULT_GRAPH_SERIES_OPTION.animationEasing,
tooltip: {
formatter: (params: any): string =>
edgeFormatter(
params.data.source,
params.data.target,
params.value,
nodes,
),
},
legend: {
...getLegendProps(legendType, legendOrientation, showLegend),
data: categoryList,
},
series,
};
return {
width,
height,
echartOptions,
};
}

View File

@@ -0,0 +1,78 @@
/**
* 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 { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries';
import { SeriesTooltipOption } from 'echarts/types/src/util/types';
import {
DEFAULT_LEGEND_FORM_DATA,
EchartsLegendFormData,
LegendOrientation,
LegendType,
} from '../types';
export type EdgeSymbol = 'none' | 'circle' | 'arrow';
export type EchartsGraphFormData = EchartsLegendFormData & {
source: string;
target: string;
sourceCategory?: string;
targetCategory?: string;
colorScheme?: string;
metric?: string;
layout?: 'none' | 'circular' | 'force';
roam: boolean | 'scale' | 'move';
draggable: boolean;
selectedMode?: boolean | 'multiple' | 'single';
showSymbolThreshold: number;
repulsion: number;
gravity: number;
baseNodeSize: number;
baseEdgeWidth: number;
edgeLength: number;
edgeSymbol: string;
friction: number;
};
export type EChartGraphNode = Omit<GraphNodeItemOption, 'value'> & {
value: number;
tooltip?: Pick<SeriesTooltipOption, 'formatter'>;
};
export const DEFAULT_FORM_DATA: EchartsGraphFormData = {
...DEFAULT_LEGEND_FORM_DATA,
source: '',
target: '',
layout: 'force',
roam: true,
draggable: false,
selectedMode: 'single',
showSymbolThreshold: 0,
repulsion: 1000,
gravity: 0.3,
edgeSymbol: 'none,arrow',
edgeLength: 400,
baseEdgeWidth: 3,
baseNodeSize: 20,
friction: 0.2,
legendOrientation: LegendOrientation.Top,
legendType: LegendType.Scroll,
};
export type tooltipFormatParams = {
data: { [name: string]: string };
};