diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Tree/Stories.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Tree/Stories.tsx new file mode 100644 index 000000000000..8b2655822eab --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Tree/Stories.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; +import { select, withKnobs, text, number } from '@storybook/addon-knobs'; +import { EchartsTreeChartPlugin } from '@superset-ui/plugin-chart-echarts'; +import transformProps from '@superset-ui/plugin-chart-echarts/lib/Tree/transformProps'; +import data from './data'; +import { withResizableChartDemo } from '../../../../shared/components/ResizableChartDemo'; + +new EchartsTreeChartPlugin().configure({ key: 'echarts-tree' }).register(); + +getChartTransformPropsRegistry().registerValue('echarts-tree', transformProps); + +export default { + title: 'Chart Plugins|plugin-chart-echarts/Tree', + decorators: [withKnobs, withResizableChartDemo], +}; + +export const Tree = ({ width, height }) => { + return ( + + ); +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Tree/data.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Tree/data.ts new file mode 100644 index 000000000000..bd992baad4ca --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Tree/data.ts @@ -0,0 +1,104 @@ +export default [ + { + id_column: '1', + parent_column: null, + name_column: 'root', + count: 10, + }, + { + id_column: '2', + parent_column: '1', + name_column: 'software', + count: 10, + }, + { + id_column: '3', + parent_column: '1', + name_column: 'hardware', + count: 10, + }, + { + id_column: '4', + parent_column: '2', + name_column: 'freeware', + count: 10, + }, + { + id_column: '5', + parent_column: '2', + name_column: 'shareware', + count: 10, + }, + { + id_column: '6', + parent_column: '2', + name_column: 'opensource', + count: 10, + }, + { + id_column: '7', + parent_column: '3', + name_column: 'computer', + count: 10, + }, + { + id_column: '8', + parent_column: '3', + name_column: 'cpu', + count: 10, + }, + { + id_column: '9', + parent_column: '3', + name_column: 'mouse', + count: 10, + }, + { + id_column: '10', + parent_column: '3', + name_column: 'keyboard', + count: 10, + }, + { + id_column: '11', + parent_column: '8', + name_column: 'intel', + count: 10, + }, + { + id_column: '12', + parent_column: '8', + name_column: 'ryzen', + count: 10, + }, + { + id_column: '13', + parent_column: '9', + name_column: 'razor', + count: 10, + }, + { + id_column: '14', + parent_column: '10', + name_column: 'Wired', + count: 10, + }, + { + id_column: '15', + parent_column: '10', + name_column: 'Wireless', + count: 10, + }, + { + id_column: '16', + parent_column: '10', + name_column: 'Ergonomic', + count: 10, + }, + { + id_column: '17', + parent_column: '10', + name_column: 'Cherry mx', + count: 10, + }, +]; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/EchartsTree.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/EchartsTree.tsx new file mode 100644 index 000000000000..4620c1fe74a7 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/EchartsTree.tsx @@ -0,0 +1,25 @@ +/** + * 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 ; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/buildQuery.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/buildQuery.ts new file mode 100644 index 000000000000..ccc398b1b8b4 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/buildQuery.ts @@ -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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; + +export default function buildQuery(formData: QueryFormData) { + return buildQueryContext(formData, { + queryFields: { + id: 'columns', + parent: 'columns', + name: 'columns', + }, + }); +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/constants.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/constants.ts new file mode 100644 index 000000000000..463835966cc7 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/constants.ts @@ -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 { TreeSeriesOption } from 'echarts'; + +export const DEFAULT_TREE_SERIES_OPTION: TreeSeriesOption = { + label: { + position: 'left', + fontSize: 15, + }, + animation: true, + animationDuration: 500, + animationEasing: 'cubicOut', + lineStyle: { color: 'source', width: 1.5 }, +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx new file mode 100644 index 000000000000..b85d1a32e18d --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx @@ -0,0 +1,280 @@ +/** + * 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'; + +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: 'id', + config: { + ...requiredEntity, + label: t('Id'), + description: t('Name of the id column'), + }, + }, + ], + [ + { + name: 'parent', + config: { + ...requiredEntity, + label: t('Parent'), + description: t('Name of the column containing the id of the parent node'), + }, + }, + ], + [ + { + name: 'name', + config: { + ...optionalEntity, + label: t('Name'), + description: t('Optional name of the data column.'), + }, + }, + ], + [ + { + // TODO: Set renderTrigger to true without getting intermittent errors in echart + name: 'root_node_id', + config: { + ...optionalEntity, + type: 'TextControl', + label: t('Root node id'), + description: t('Id of root node of the tree.'), + }, + }, + ], + [ + { + name: 'metric', + config: { + ...optionalEntity, + type: 'MetricsControl', + label: t('Metric'), + description: t('Metric for node values'), + }, + }, + ], + ['adhoc_filters'], + ['row_limit'], + ], + }, + { + label: t('Chart options'), + expanded: true, + controlSetRows: [ + [

{t('Layout')}

], + [ + { + name: 'layout', + config: { + type: 'RadioButtonControl', + renderTrigger: true, + label: t('Tree layout'), + default: DEFAULT_FORM_DATA.layout, + options: [ + ['orthogonal', t('Orthogonal')], + ['radial', t('Radial')], + ], + description: t('Layout type of tree'), + }, + }, + ], + + [ + { + name: 'orient', + config: { + type: 'RadioButtonControl', + renderTrigger: true, + label: t('Tree orientation'), + default: DEFAULT_FORM_DATA.orient, + options: [ + ['LR', t('Left to Right')], + ['RL', t('Right to Left')], + ['TB', t('Top to Bottom')], + ['BT', t('Bottom to Top')], + ], + description: t('Orientation of tree'), + visibility({ form_data: { layout } }) { + return (layout || DEFAULT_FORM_DATA.layout) === 'orthogonal'; + }, + }, + }, + ], + [ + { + name: 'node_label_position', + config: { + type: 'RadioButtonControl', + renderTrigger: true, + label: t('Node label position'), + default: DEFAULT_FORM_DATA.nodeLabelPosition, + options: [ + ['left', t('left')], + ['top', t('top')], + ['right', t('right')], + ['bottom', t('bottom')], + ], + description: t('Position of intermidiate node label on tree'), + }, + }, + ], + [ + { + name: 'child_label_position', + config: { + type: 'RadioButtonControl', + renderTrigger: true, + label: t('Child label position'), + default: DEFAULT_FORM_DATA.childLabelPosition, + options: [ + ['left', t('left')], + ['top', t('top')], + ['right', t('right')], + ['bottom', t('bottom')], + ], + description: t('Position of child node label on tree'), + }, + }, + ], + [ + { + name: 'emphasis', + config: { + type: 'RadioButtonControl', + renderTrigger: true, + label: t('Emphasis'), + default: DEFAULT_FORM_DATA.emphasis, + options: [ + ['ancestor', t('ancestor')], + ['descendant', t('descendant')], + ], + description: t('Which relatives to highlight on hover'), + visibility({ form_data: { layout } }) { + return (layout || DEFAULT_FORM_DATA.layout) === 'orthogonal'; + }, + }, + }, + ], + [ + { + name: 'symbol', + config: { + type: 'SelectControl', + renderTrigger: true, + label: t('Symbol'), + default: DEFAULT_FORM_DATA.symbol, + options: [ + { + label: t('Empty circle'), + value: 'emptyCircle', + }, + { + label: t('Circle'), + value: 'circle', + }, + { + label: t('Rectangle'), + value: 'rect', + }, + { + label: t('Triangle'), + value: 'triangle', + }, + { + label: t('Diamond'), + value: 'diamond', + }, + { + label: t('Pin'), + value: 'pin', + }, + { + label: t('Arrow'), + value: 'arrow', + }, + { + label: t('None'), + value: 'none', + }, + ], + description: t('Layout type of tree'), + }, + }, + ], + [ + { + name: 'symbolSize', + config: { + type: 'SliderControl', + label: t('Symbol size'), + renderTrigger: true, + min: 5, + max: 30, + step: 2, + default: DEFAULT_FORM_DATA.symbolSize, + description: t('Size of edge symbols'), + }, + }, + ], + [ + { + 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.'), + }, + }, + ], + ], + }, + ], +}; + +export default controlPanel; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/images/thumbnail.png b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/images/thumbnail.png new file mode 100644 index 000000000000..8ee0953361c7 Binary files /dev/null and b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/images/thumbnail.png differ diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/index.ts new file mode 100644 index 000000000000..6cb6fd01da16 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/index.ts @@ -0,0 +1,39 @@ +/** + * 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 EchartsTreeChartPlugin extends ChartPlugin { + constructor() { + super({ + buildQuery, + controlPanel, + loadChart: () => import('./EchartsTree'), + metadata: new ChartMetadata({ + credits: ['https://echarts.apache.org'], + name: t('Tree Chart'), + thumbnail, + }), + transformProps, + }); + } +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/transformProps.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/transformProps.ts new file mode 100644 index 000000000000..d7904428d7eb --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/transformProps.ts @@ -0,0 +1,190 @@ +/** + * 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 { ChartProps, getMetricLabel, DataRecordValue } from '@superset-ui/core'; +import { EChartsOption, TreeSeriesOption } from 'echarts'; +import { TreeSeriesNodeItemOption } from 'echarts/types/src/chart/tree/TreeSeries'; +import { OptionName } from 'echarts/types/src/util/types'; +import { + EchartsTreeFormData, + DEFAULT_FORM_DATA as DEFAULT_GRAPH_FORM_DATA, + TreeDataRecord, +} from './types'; +import { DEFAULT_TREE_SERIES_OPTION } from './constants'; +import { EchartsProps } from '../types'; + +export default function transformProps(chartProps: ChartProps): EchartsProps { + const { width, height, formData, queriesData } = chartProps; + const data: TreeDataRecord[] = queriesData[0].data || []; + + const { + id, + parent, + name, + metric = '', + rootNodeId, + layout, + orient, + symbol, + symbolSize, + roam, + nodeLabelPosition, + childLabelPosition, + emphasis, + }: EchartsTreeFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData }; + const metricLabel = getMetricLabel(metric); + + const nameColumn = name || id; + + function findNodeName(rootNodeId: DataRecordValue): OptionName { + let nodeName: DataRecordValue = ''; + data.some(node => { + if (node[id]!.toString() === rootNodeId) { + nodeName = node[nameColumn]; + return true; + } + return false; + }); + return nodeName; + } + + function getTotalChildren(tree: TreeSeriesNodeItemOption) { + let totalChildren = 0; + + function traverse(tree: TreeSeriesNodeItemOption) { + tree.children!.forEach(node => { + traverse(node); + }); + totalChildren += 1; + } + traverse(tree); + return totalChildren; + } + + function createTree(rootNodeId: DataRecordValue): TreeSeriesNodeItemOption { + const rootNodeName = findNodeName(rootNodeId); + const tree: TreeSeriesNodeItemOption = { name: rootNodeName, children: [] }; + const children: TreeSeriesNodeItemOption[][] = []; + const indexMap: { [name: string]: number } = {}; + + if (!rootNodeName) { + return tree; + } + + // index indexMap with node ids + for (let i = 0; i < data.length; i += 1) { + const nodeId = data[i][id] as number; + indexMap[nodeId] = i; + children[i] = []; + } + + // generate tree + for (let i = 0; i < data.length; i += 1) { + const node = data[i]; + if (node[parent] === rootNodeId) { + tree.children?.push({ + name: node[nameColumn], + children: children[i], + value: node[metricLabel], + }); + } else { + const parentId = node[parent]; + if (data[indexMap[parentId]]) { + const parentIndex = indexMap[parentId]; + children[parentIndex].push({ + name: node[nameColumn], + children: children[i], + value: node[metricLabel], + }); + } + } + } + + return tree; + } + + let finalTree = {}; + + if (rootNodeId) { + finalTree = createTree(rootNodeId); + } else { + /* + to select root node, + 1.find parent nodes with only 1 child. + 2.build tree for each such child nodes as root + 3.select tree with most children + */ + // create map of parent:children + const parentChildMap: { [name: string]: { [name: string]: any } } = {}; + data.forEach(node => { + const parentId = node[parent] as string; + if (parentId in parentChildMap) { + parentChildMap[parentId].push({ id: node[id] }); + } else { + parentChildMap[parentId] = [{ id: node[id] }]; + } + }); + + // for each parent node which has only 1 child,find tree and select node with max number of children. + let maxChildren = 0; + Object.keys(parentChildMap).forEach(key => { + if (parentChildMap[key].length === 1) { + const tree = createTree(parentChildMap[key][0].id); + const totalChildren = getTotalChildren(tree); + if (totalChildren > maxChildren) { + maxChildren = totalChildren; + finalTree = tree; + } + } + }); + } + + const series: TreeSeriesOption[] = [ + { + type: 'tree', + data: [finalTree], + label: { ...DEFAULT_TREE_SERIES_OPTION.label, position: nodeLabelPosition }, + emphasis: { focus: emphasis }, + animation: DEFAULT_TREE_SERIES_OPTION.animation, + layout, + orient, + symbol, + roam, + symbolSize, + lineStyle: DEFAULT_TREE_SERIES_OPTION.lineStyle, + select: DEFAULT_TREE_SERIES_OPTION.select, + leaves: { label: { position: childLabelPosition } }, + }, + ]; + + const echartOptions: EChartsOption = { + animationDuration: DEFAULT_TREE_SERIES_OPTION.animationDuration, + animationEasing: DEFAULT_TREE_SERIES_OPTION.animationEasing, + series, + tooltip: { + trigger: 'item', + triggerOn: 'mousemove', + }, + }; + + return { + width, + height, + echartOptions, + }; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/types.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/types.ts new file mode 100644 index 000000000000..81db2d59508f --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Tree/types.ts @@ -0,0 +1,55 @@ +/** + * 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 { TreeSeriesNodeItemOption } from 'echarts/types/src/chart/tree/TreeSeries'; + +export type EchartsTreeFormData = { + id: string; + parent: string; + name: string; + rootNodeId?: string | number; + orient: 'LR' | 'RL' | 'TB' | 'BT'; + symbol: string; + symbolSize: number; + colorScheme?: string; + metric?: string; + layout: 'orthogonal' | 'radial'; + roam: boolean | 'scale' | 'move'; + nodeLabelPosition: 'top' | 'bottom' | 'left' | 'right'; + childLabelPosition: 'top' | 'bottom' | 'left' | 'right'; + emphasis: 'none' | 'ancestor' | 'descendant'; +}; + +export const DEFAULT_FORM_DATA: EchartsTreeFormData = { + id: '', + parent: '', + name: '', + rootNodeId: '', + layout: 'orthogonal', + orient: 'LR', + symbol: 'emptyCircle', + symbolSize: 7, + roam: true, + nodeLabelPosition: 'left', + childLabelPosition: 'bottom', + emphasis: 'descendant', +}; + +export type TreeDataRecord = Record & { + children: TreeSeriesNodeItemOption[]; +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/index.ts index 264170457ab6..7006f1cf7dbf 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/index.ts @@ -24,6 +24,7 @@ export { default as EchartsGraphChartPlugin } from './Graph'; export { default as EchartsGaugeChartPlugin } from './Gauge'; export { default as EchartsRadarChartPlugin } from './Radar'; export { default as EchartsFunnelChartPlugin } from './Funnel'; +export { default as EchartsTreeChartPlugin } from './Tree'; /** * Note: this file exports the default export from EchartsTimeseries.tsx. diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Tree/buildQuery.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Tree/buildQuery.test.ts new file mode 100644 index 000000000000..38006defde52 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Tree/buildQuery.test.ts @@ -0,0 +1,51 @@ +/** + * 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 buildQuery from '../../src/Tree/buildQuery'; + +describe('Tree buildQuery', () => { + it('should build query', () => { + const formData = { + datasource: '5__table', + granularity_sqla: 'ds', + id: 'id_col', + parent: 'relation_col', + name: 'name_col', + metrics: ['foo', 'bar'], + viz_type: 'my_chart', + }; + const queryContext = buildQuery(formData); + const [query] = queryContext.queries; + expect(query.columns).toEqual(['id_col', 'relation_col', 'name_col']); + expect(query.metrics).toEqual(['foo', 'bar']); + }); + it('should build query without name column', () => { + const formData = { + datasource: '5__table', + granularity_sqla: 'ds', + id: 'id_col', + parent: 'relation_col', + metrics: ['foo', 'bar'], + viz_type: 'my_chart', + }; + const queryContext = buildQuery(formData); + const [query] = queryContext.queries; + expect(query.columns).toEqual(['id_col', 'relation_col']); + expect(query.metrics).toEqual(['foo', 'bar']); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Tree/transformProps.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Tree/transformProps.test.ts new file mode 100644 index 000000000000..ce84ca612123 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/test/Tree/transformProps.test.ts @@ -0,0 +1,418 @@ +/** + * 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 { ChartProps } from '@superset-ui/core'; +import transformProps from '../../src/Tree/transformProps'; + +describe('EchartsTree tranformProps', () => { + const formData = { + colorScheme: 'bnbColors', + datasource: '3__table', + granularity_sqla: 'ds', + metric: 'count', + id: 'id_column', + parent: 'relation_column', + name: 'name_column', + rootNodeId: '1', + }; + const chartPropsConfig = { + formData, + width: 800, + height: 600, + }; + it('should tranform when parent present before child', () => { + const queriesData = [ + { + colnames: ['id_column', 'relation_column', 'name_column', 'count'], + data: [ + { + id_column: '1', + relation_column: null, + name_column: 'root', + count: 10, + }, + { + id_column: '2', + relation_column: '1', + name_column: 'first_child', + count: 10, + }, + { + id_column: '3', + relation_column: '2', + name_column: 'second_child', + count: 10, + }, + { + id_column: '4', + relation_column: '3', + name_column: 'third_child', + count: 10, + }, + ], + }, + ]; + + const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); + expect(transformProps(chartProps)).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + data: [ + { + name: 'root', + children: [ + { + name: 'first_child', + value: 10, + children: [ + { + name: 'second_child', + value: 10, + children: [{ name: 'third_child', value: 10, children: [] }], + }, + ], + }, + ], + }, + ], + }), + ]), + }), + }), + ); + }); + it('should tranform when child is present before parent', () => { + const queriesData = [ + { + colnames: ['id_column', 'relation_column', 'name_column', 'count'], + data: [ + { + id_column: '1', + relation_column: null, + name_column: 'root', + count: 10, + }, + { + id_column: '2', + relation_column: '4', + name_column: 'second_child', + count: 20, + }, + { + id_column: '3', + relation_column: '4', + name_column: 'second_child', + count: 30, + }, + { + id_column: '4', + relation_column: '1', + name_column: 'first_child', + count: 40, + }, + ], + }, + ]; + + const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); + expect(transformProps(chartProps)).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + data: [ + { + name: 'root', + children: [ + { + name: 'first_child', + value: 40, + children: [ + { + name: 'second_child', + value: 20, + children: [], + }, + { + name: 'second_child', + value: 30, + children: [], + }, + ], + }, + ], + }, + ], + }), + ]), + }), + }), + ); + }); + it('ignore node if not attached to root', () => { + const formData = { + colorScheme: 'bnbColors', + datasource: '3__table', + granularity_sqla: 'ds', + metric: 'count', + id: 'id_column', + parent: 'relation_column', + name: 'name_column', + rootNodeId: '2', + }; + const chartPropsConfig = { + formData, + width: 800, + height: 600, + }; + const queriesData = [ + { + colnames: ['id_column', 'relation_column', 'name_column', 'count'], + data: [ + { + id_column: '1', + relation_column: null, + name_column: 'root', + count: 10, + }, + { + id_column: '2', + relation_column: '1', + name_column: 'first_child', + count: 10, + }, + { + id_column: '3', + relation_column: '2', + name_column: 'second_child', + count: 10, + }, + { + id_column: '4', + relation_column: '3', + name_column: 'third_child', + count: 20, + }, + ], + }, + ]; + + const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); + expect(transformProps(chartProps)).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + data: [ + { + name: 'first_child', + children: [ + { + name: 'second_child', + value: 10, + children: [ + { + name: 'third_child', + value: 20, + children: [], + }, + ], + }, + ], + }, + ], + }), + ]), + }), + }), + ); + }); + it('should transform props if name column is not specified', () => { + const formData = { + colorScheme: 'bnbColors', + datasource: '3__table', + granularity_sqla: 'ds', + metric: 'count', + id: 'id_column', + parent: 'relation_column', + rootNodeId: '1', + }; + const chartPropsConfig = { + formData, + width: 800, + height: 600, + }; + const queriesData = [ + { + colnames: ['id_column', 'relation_column', 'count'], + data: [ + { + id_column: '1', + relation_column: null, + count: 10, + }, + { + id_column: '2', + relation_column: '1', + count: 10, + }, + { + id_column: '3', + relation_column: '2', + count: 10, + }, + { + id_column: '4', + relation_column: '3', + count: 20, + }, + ], + }, + ]; + + const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); + expect(transformProps(chartProps)).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + data: [ + { + name: '1', + children: [ + { + name: '2', + value: 10, + children: [ + { + name: '3', + value: 10, + children: [ + { + name: '4', + value: 20, + children: [], + }, + ], + }, + ], + }, + ], + }, + ], + }), + ]), + }), + }), + ); + }); + it('should find root node with null parent when root node name is not provided', () => { + const formData = { + colorScheme: 'bnbColors', + datasource: '3__table', + granularity_sqla: 'ds', + metric: 'count', + id: 'id_column', + parent: 'relation_column', + name: 'name_column', + }; + const chartPropsConfig = { + formData, + width: 800, + height: 600, + }; + const queriesData = [ + { + colnames: ['id_column', 'relation_column', 'name_column', 'count'], + data: [ + { + id_column: '2', + relation_column: '4', + name_column: 'second_child', + count: 20, + }, + { + id_column: '3', + relation_column: '4', + name_column: 'second_child', + count: 30, + }, + { + id_column: '4', + relation_column: '1', + name_column: 'first_child', + count: 40, + }, + { + id_column: '1', + relation_column: null, + name_column: 'root', + count: 10, + }, + ], + }, + ]; + + const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); + expect(transformProps(chartProps)).toEqual( + expect.objectContaining({ + width: 800, + height: 600, + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + data: [ + { + name: 'root', + children: [ + { + name: 'first_child', + value: 40, + children: [ + { + name: 'second_child', + value: 20, + children: [], + }, + { + name: 'second_child', + value: 30, + children: [], + }, + ], + }, + ], + }, + ], + }), + ]), + }), + }), + ); + }); +});