Spaces:
Running
Running
| /* | |
| * 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; |