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: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }),
+ ]),
+ }),
+ }),
+ );
+ });
+});