| /* | 
 | * 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. | 
 | */ | 
 |  | 
 | /** | 
 |  * Tree data structure | 
 |  */ | 
 |  | 
 | import * as zrUtil from 'zrender/src/core/util'; | 
 | import Model from '../model/Model'; | 
 | import linkSeriesData from './helper/linkSeriesData'; | 
 | import SeriesData from './SeriesData'; | 
 | import prepareSeriesDataSchema from './helper/createDimensions'; | 
 | import { | 
 |     DimensionLoose, ParsedValue, OptionDataValue, | 
 |     OptionDataItemObject | 
 | } from '../util/types'; | 
 | import { Dictionary } from 'zrender/src/core/types'; | 
 | import { convertOptionIdName } from '../util/model'; | 
 |  | 
 | type TreeTraverseOrder = 'preorder' | 'postorder'; | 
 | type TreeTraverseCallback<Ctx> = (this: Ctx, node: TreeNode) => boolean | void; | 
 | type TreeTraverseOption = { | 
 |     order?: TreeTraverseOrder | 
 |     attr?: 'children' | 'viewChildren' | 
 | }; | 
 |  | 
 | interface TreeNodeOption extends Pick<OptionDataItemObject<OptionDataValue>, 'name' | 'value'> { | 
 |     children?: TreeNodeOption[]; | 
 | } | 
 |  | 
 | export class TreeNode { | 
 |     name: string; | 
 |  | 
 |     depth: number = 0; | 
 |  | 
 |     height: number = 0; | 
 |  | 
 |     parentNode: TreeNode; | 
 |     /** | 
 |      * Reference to list item. | 
 |      * Do not persistent dataIndex outside, | 
 |      * besause it may be changed by list. | 
 |      * If dataIndex -1, | 
 |      * this node is logical deleted (filtered) in list. | 
 |      */ | 
 |     dataIndex: number = -1; | 
 |  | 
 |     children: TreeNode[] = []; | 
 |  | 
 |     viewChildren: TreeNode[] = []; | 
 |  | 
 |     isExpand: boolean = false; | 
 |  | 
 |     readonly hostTree: Tree<Model>; | 
 |  | 
 |     constructor(name: string, hostTree: Tree<Model>) { | 
 |         this.name = name || ''; | 
 |  | 
 |         this.hostTree = hostTree; | 
 |     } | 
 |     /** | 
 |      * The node is removed. | 
 |      */ | 
 |     isRemoved(): boolean { | 
 |         return this.dataIndex < 0; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Travel this subtree (include this node). | 
 |      * Usage: | 
 |      *    node.eachNode(function () { ... }); // preorder | 
 |      *    node.eachNode('preorder', function () { ... }); // preorder | 
 |      *    node.eachNode('postorder', function () { ... }); // postorder | 
 |      *    node.eachNode( | 
 |      *        {order: 'postorder', attr: 'viewChildren'}, | 
 |      *        function () { ... } | 
 |      *    ); // postorder | 
 |      * | 
 |      * @param options If string, means order. | 
 |      * @param options.order 'preorder' or 'postorder' | 
 |      * @param options.attr 'children' or 'viewChildren' | 
 |      * @param cb If in preorder and return false, | 
 |      *                      its subtree will not be visited. | 
 |      */ | 
 |     eachNode<Ctx>(options: TreeTraverseOrder, cb: TreeTraverseCallback<Ctx>, context?: Ctx): void; | 
 |     eachNode<Ctx>(options: TreeTraverseOption, cb: TreeTraverseCallback<Ctx>, context?: Ctx): void; | 
 |     eachNode<Ctx>(cb: TreeTraverseCallback<Ctx>, context?: Ctx): void; | 
 |     eachNode<Ctx>( | 
 |         options: TreeTraverseOrder | TreeTraverseOption | TreeTraverseCallback<Ctx>, | 
 |         cb?: TreeTraverseCallback<Ctx> | Ctx, | 
 |         context?: Ctx | 
 |     ) { | 
 |         if (zrUtil.isFunction(options)) { | 
 |             context = cb as Ctx; | 
 |             cb = options; | 
 |             options = null; | 
 |         } | 
 |  | 
 |         options = options || {}; | 
 |         if (zrUtil.isString(options)) { | 
 |             options = {order: options}; | 
 |         } | 
 |  | 
 |         const order = (options as TreeTraverseOption).order || 'preorder'; | 
 |         const children = this[(options as TreeTraverseOption).attr || 'children']; | 
 |  | 
 |         let suppressVisitSub; | 
 |         order === 'preorder' && (suppressVisitSub = (cb as TreeTraverseCallback<Ctx>).call(context as Ctx, this)); | 
 |  | 
 |         for (let i = 0; !suppressVisitSub && i < children.length; i++) { | 
 |             children[i].eachNode( | 
 |                 options as TreeTraverseOption, | 
 |                 cb as TreeTraverseCallback<Ctx>, | 
 |                 context | 
 |             ); | 
 |         } | 
 |  | 
 |         order === 'postorder' && (cb as TreeTraverseCallback<Ctx>).call(context, this); | 
 |     } | 
 |  | 
 |     /** | 
 |      * Update depth and height of this subtree. | 
 |      */ | 
 |     updateDepthAndHeight(depth: number) { | 
 |         let height = 0; | 
 |         this.depth = depth; | 
 |         for (let i = 0; i < this.children.length; i++) { | 
 |             const child = this.children[i]; | 
 |             child.updateDepthAndHeight(depth + 1); | 
 |             if (child.height > height) { | 
 |                 height = child.height; | 
 |             } | 
 |         } | 
 |         this.height = height + 1; | 
 |     } | 
 |  | 
 |     getNodeById(id: string): TreeNode { | 
 |         if (this.getId() === id) { | 
 |             return this; | 
 |         } | 
 |         for (let i = 0, children = this.children, len = children.length; i < len; i++) { | 
 |             const res = children[i].getNodeById(id); | 
 |             if (res) { | 
 |                 return res; | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     contains(node: TreeNode): boolean { | 
 |         if (node === this) { | 
 |             return true; | 
 |         } | 
 |         for (let i = 0, children = this.children, len = children.length; i < len; i++) { | 
 |             const res = children[i].contains(node); | 
 |             if (res) { | 
 |                 return res; | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * @param includeSelf Default false. | 
 |      * @return order: [root, child, grandchild, ...] | 
 |      */ | 
 |     getAncestors(includeSelf?: boolean): TreeNode[] { | 
 |         const ancestors = []; | 
 |         let node = includeSelf ? this : this.parentNode; | 
 |         while (node) { | 
 |             ancestors.push(node); | 
 |             node = node.parentNode; | 
 |         } | 
 |         ancestors.reverse(); | 
 |         return ancestors; | 
 |     } | 
 |  | 
 |     getAncestorsIndices(): number[] { | 
 |         const indices: number[] = []; | 
 |         let currNode = this as TreeNode; | 
 |         while (currNode) { | 
 |             indices.push(currNode.dataIndex); | 
 |             currNode = currNode.parentNode; | 
 |         } | 
 |         indices.reverse(); | 
 |         return indices; | 
 |     } | 
 |  | 
 |     getDescendantIndices(): number[] { | 
 |         const indices: number[] = []; | 
 |         this.eachNode(childNode => { | 
 |             indices.push(childNode.dataIndex); | 
 |         }); | 
 |         return indices; | 
 |     } | 
 |  | 
 |     getValue(dimension?: DimensionLoose): ParsedValue { | 
 |         const data = this.hostTree.data; | 
 |         return data.getStore().get(data.getDimensionIndex(dimension || 'value'), this.dataIndex); | 
 |     } | 
 |  | 
 |     setLayout(layout: any, merge?: boolean) { | 
 |         this.dataIndex >= 0 | 
 |             && this.hostTree.data.setItemLayout(this.dataIndex, layout, merge); | 
 |     } | 
 |  | 
 |     /** | 
 |      * @return {Object} layout | 
 |      */ | 
 |     getLayout(): any { | 
 |         return this.hostTree.data.getItemLayout(this.dataIndex); | 
 |     } | 
 |  | 
 |     getModel<T = unknown>(): Model<T>; | 
 |     // @depcrecated | 
 |     // getModel<T = unknown, S extends keyof T = keyof T>(path: S): Model<T[S]> | 
 |     // eslint-disable-next-line @typescript-eslint/no-unused-vars | 
 |     getModel<T = unknown>(path?: string): Model { | 
 |         if (this.dataIndex < 0) { | 
 |             return; | 
 |         } | 
 |         const hostTree = this.hostTree; | 
 |         const itemModel = hostTree.data.getItemModel(this.dataIndex); | 
 |         return itemModel.getModel(path as any); | 
 |     } | 
 |  | 
 |     // TODO: TYPE More specific model | 
 |     getLevelModel(): Model { | 
 |         return (this.hostTree.levelModels || [])[this.depth]; | 
 |     } | 
 |  | 
 |     /** | 
 |      * @example | 
 |      *  setItemVisual('color', color); | 
 |      *  setItemVisual({ | 
 |      *      'color': color | 
 |      *  }); | 
 |      */ | 
 |     // TODO: TYPE | 
 |     setVisual(key: string, value: any): void; | 
 |     setVisual(obj: Dictionary<any>): void; | 
 |     setVisual(key: string | Dictionary<any>, value?: any) { | 
 |         this.dataIndex >= 0 | 
 |             && this.hostTree.data.setItemVisual(this.dataIndex, key as any, value); | 
 |     } | 
 |  | 
 |     /** | 
 |      * Get item visual | 
 |      * FIXME: make return type better | 
 |      */ | 
 |     getVisual(key: string): unknown { | 
 |         return this.hostTree.data.getItemVisual(this.dataIndex, key as any); | 
 |     } | 
 |  | 
 |     getRawIndex(): number { | 
 |         return this.hostTree.data.getRawIndex(this.dataIndex); | 
 |     } | 
 |  | 
 |     getId(): string { | 
 |         return this.hostTree.data.getId(this.dataIndex); | 
 |     } | 
 |  | 
 |     /** | 
 |      * index in parent's children | 
 |      */ | 
 |     getChildIndex(): number { | 
 |         if (this.parentNode) { | 
 |             const children = this.parentNode.children; | 
 |             for (let i = 0; i < children.length; ++i) { | 
 |                 if (children[i] === this) { | 
 |                     return i; | 
 |                 } | 
 |             } | 
 |             return -1; | 
 |         } | 
 |         return -1; | 
 |     } | 
 |  | 
 |     /** | 
 |      * if this is an ancestor of another node | 
 |      * | 
 |      * @param node another node | 
 |      * @return if is ancestor | 
 |      */ | 
 |     isAncestorOf(node: TreeNode): boolean { | 
 |         let parent = node.parentNode; | 
 |         while (parent) { | 
 |             if (parent === this) { | 
 |                 return true; | 
 |             } | 
 |             parent = parent.parentNode; | 
 |         } | 
 |         return false; | 
 |     } | 
 |  | 
 |     /** | 
 |      * if this is an descendant of another node | 
 |      * | 
 |      * @param node another node | 
 |      * @return if is descendant | 
 |      */ | 
 |     isDescendantOf(node: TreeNode): boolean { | 
 |         return node !== this && node.isAncestorOf(this); | 
 |     } | 
 | }; | 
 |  | 
 | class Tree<HostModel extends Model = Model, LevelOption = any> { | 
 |  | 
 |     type: 'tree' = 'tree'; | 
 |  | 
 |     root: TreeNode; | 
 |  | 
 |     data: SeriesData; | 
 |  | 
 |     hostModel: HostModel; | 
 |  | 
 |     levelModels: Model<LevelOption>[]; | 
 |  | 
 |     private _nodes: TreeNode[] = []; | 
 |  | 
 |     constructor(hostModel: HostModel) { | 
 |  | 
 |         this.hostModel = hostModel; | 
 |     } | 
 |     /** | 
 |      * Travel this subtree (include this node). | 
 |      * Usage: | 
 |      *    node.eachNode(function () { ... }); // preorder | 
 |      *    node.eachNode('preorder', function () { ... }); // preorder | 
 |      *    node.eachNode('postorder', function () { ... }); // postorder | 
 |      *    node.eachNode( | 
 |      *        {order: 'postorder', attr: 'viewChildren'}, | 
 |      *        function () { ... } | 
 |      *    ); // postorder | 
 |      * | 
 |      * @param options If string, means order. | 
 |      * @param options.order 'preorder' or 'postorder' | 
 |      * @param options.attr 'children' or 'viewChildren' | 
 |      * @param cb | 
 |      * @param context | 
 |      */ | 
 |     eachNode<Ctx>(options: TreeTraverseOrder, cb: TreeTraverseCallback<Ctx>, context?: Ctx): void; | 
 |     eachNode<Ctx>(options: TreeTraverseOption, cb: TreeTraverseCallback<Ctx>, context?: Ctx): void; | 
 |     eachNode<Ctx>(cb: TreeTraverseCallback<Ctx>, context?: Ctx): void; | 
 |     eachNode<Ctx>( | 
 |         options: TreeTraverseOrder | TreeTraverseOption | TreeTraverseCallback<Ctx>, | 
 |         cb?: TreeTraverseCallback<Ctx> | Ctx, | 
 |         context?: Ctx | 
 |     ) { | 
 |         this.root.eachNode(options as TreeTraverseOption, cb as TreeTraverseCallback<Ctx>, context); | 
 |     } | 
 |  | 
 |     getNodeByDataIndex(dataIndex: number): TreeNode { | 
 |         const rawIndex = this.data.getRawIndex(dataIndex); | 
 |         return this._nodes[rawIndex]; | 
 |     } | 
 |  | 
 |     getNodeById(name: string): TreeNode { | 
 |         return this.root.getNodeById(name); | 
 |     } | 
 |  | 
 |     /** | 
 |      * Update item available by list, | 
 |      * when list has been performed options like 'filterSelf' or 'map'. | 
 |      */ | 
 |     update() { | 
 |         const data = this.data; | 
 |         const nodes = this._nodes; | 
 |  | 
 |         for (let i = 0, len = nodes.length; i < len; i++) { | 
 |             nodes[i].dataIndex = -1; | 
 |         } | 
 |  | 
 |         for (let i = 0, len = data.count(); i < len; i++) { | 
 |             nodes[data.getRawIndex(i)].dataIndex = i; | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * Clear all layouts | 
 |      */ | 
 |     clearLayouts() { | 
 |         this.data.clearItemLayouts(); | 
 |     } | 
 |  | 
 |  | 
 |     /** | 
 |      * data node format: | 
 |      * { | 
 |      *     name: ... | 
 |      *     value: ... | 
 |      *     children: [ | 
 |      *         { | 
 |      *             name: ... | 
 |      *             value: ... | 
 |      *             children: ... | 
 |      *         }, | 
 |      *         ... | 
 |      *     ] | 
 |      * } | 
 |      */ | 
 |     static createTree<T extends TreeNodeOption, HostModel extends Model>( | 
 |         dataRoot: T, | 
 |         hostModel: HostModel, | 
 |         beforeLink?: (data: SeriesData) => void | 
 |     ) { | 
 |  | 
 |         const tree = new Tree(hostModel); | 
 |         const listData: TreeNodeOption[] = []; | 
 |         let dimMax = 1; | 
 |  | 
 |         buildHierarchy(dataRoot); | 
 |  | 
 |         function buildHierarchy(dataNode: TreeNodeOption, parentNode?: TreeNode) { | 
 |             const value = dataNode.value; | 
 |             dimMax = Math.max(dimMax, zrUtil.isArray(value) ? value.length : 1); | 
 |  | 
 |             listData.push(dataNode); | 
 |  | 
 |             const node = new TreeNode(convertOptionIdName(dataNode.name, ''), tree); | 
 |             parentNode | 
 |                 ? addChild(node, parentNode) | 
 |                 : (tree.root = node); | 
 |  | 
 |             tree._nodes.push(node); | 
 |  | 
 |             const children = dataNode.children; | 
 |             if (children) { | 
 |                 for (let i = 0; i < children.length; i++) { | 
 |                     buildHierarchy(children[i], node); | 
 |                 } | 
 |             } | 
 |         } | 
 |  | 
 |         tree.root.updateDepthAndHeight(0); | 
 |  | 
 |         const { dimensions } = prepareSeriesDataSchema(listData, { | 
 |             coordDimensions: ['value'], | 
 |             dimensionsCount: dimMax | 
 |         }); | 
 |  | 
 |         const list = new SeriesData(dimensions, hostModel); | 
 |         list.initData(listData); | 
 |  | 
 |         beforeLink && beforeLink(list); | 
 |  | 
 |         linkSeriesData({ | 
 |             mainData: list, | 
 |             struct: tree, | 
 |             structAttr: 'tree' | 
 |         }); | 
 |  | 
 |         tree.update(); | 
 |  | 
 |         return tree; | 
 |     } | 
 |  | 
 | } | 
 |  | 
 | /** | 
 |  * It is needed to consider the mess of 'list', 'hostModel' when creating a TreeNote, | 
 |  * so this function is not ready and not necessary to be public. | 
 |  */ | 
 | function addChild(child: TreeNode, node: TreeNode) { | 
 |     const children = node.children; | 
 |     if (child.parentNode === node) { | 
 |         return; | 
 |     } | 
 |  | 
 |     children.push(child); | 
 |     child.parentNode = node; | 
 | } | 
 |  | 
 | export default Tree; |