blob: 84d16801098d0636c188745251d3a086dd558d97 [file] [log] [blame]
/*
* 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
*
* @module echarts/data/Tree
*/
import * as zrUtil from 'zrender/src/core/util';
import Model from '../model/Model';
import linkList from './helper/linkList';
import List from './List';
import createDimensions from './helper/createDimensions';
/**
* @constructor module:echarts/data/Tree~TreeNode
* @param {string} name
* @param {module:echarts/data/Tree} hostTree
*/
var TreeNode = function (name, hostTree) {
/**
* @type {string}
*/
this.name = name || '';
/**
* Depth of node
*
* @type {number}
* @readOnly
*/
this.depth = 0;
/**
* Height of the subtree rooted at this node.
* @type {number}
* @readOnly
*/
this.height = 0;
/**
* @type {module:echarts/data/Tree~TreeNode}
* @readOnly
*/
this.parentNode = null;
/**
* 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.
*
* @type {Object}
* @readOnly
*/
this.dataIndex = -1;
/**
* @type {Array.<module:echarts/data/Tree~TreeNode>}
* @readOnly
*/
this.children = [];
/**
* @type {Array.<module:echarts/data/Tree~TreeNode>}
* @pubilc
*/
this.viewChildren = [];
/**
* @type {moduel:echarts/data/Tree}
* @readOnly
*/
this.hostTree = hostTree;
};
TreeNode.prototype = {
constructor: TreeNode,
/**
* The node is removed.
* @return {boolean} is removed.
*/
isRemoved: function () {
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 {(Object|string)} options If string, means order.
* @param {string=} options.order 'preorder' or 'postorder'
* @param {string=} options.attr 'children' or 'viewChildren'
* @param {Function} cb If in preorder and return false,
* its subtree will not be visited.
* @param {Object} [context]
*/
eachNode: function (options, cb, context) {
if (typeof options === 'function') {
context = cb;
cb = options;
options = null;
}
options = options || {};
if (zrUtil.isString(options)) {
options = {
order: options
};
}
var order = options.order || 'preorder';
var children = this[options.attr || 'children'];
var suppressVisitSub;
order === 'preorder' && (suppressVisitSub = cb.call(context, this));
for (var i = 0; !suppressVisitSub && i < children.length; i++) {
children[i].eachNode(options, cb, context);
}
order === 'postorder' && cb.call(context, this);
},
/**
* Update depth and height of this subtree.
*
* @param {number} depth
*/
updateDepthAndHeight: function (depth) {
var height = 0;
this.depth = depth;
for (var i = 0; i < this.children.length; i++) {
var child = this.children[i];
child.updateDepthAndHeight(depth + 1);
if (child.height > height) {
height = child.height;
}
}
this.height = height + 1;
},
/**
* @param {string} id
* @return {module:echarts/data/Tree~TreeNode}
*/
getNodeById: function (id) {
if (this.getId() === id) {
return this;
}
for (var i = 0, children = this.children, len = children.length; i < len; i++) {
var res = children[i].getNodeById(id);
if (res) {
return res;
}
}
},
/**
* @param {module:echarts/data/Tree~TreeNode} node
* @return {boolean}
*/
contains: function (node) {
if (node === this) {
return true;
}
for (var i = 0, children = this.children, len = children.length; i < len; i++) {
var res = children[i].contains(node);
if (res) {
return res;
}
}
},
/**
* @param {boolean} includeSelf Default false.
* @return {Array.<module:echarts/data/Tree~TreeNode>} order: [root, child, grandchild, ...]
*/
getAncestors: function (includeSelf) {
var ancestors = [];
var node = includeSelf ? this : this.parentNode;
while (node) {
ancestors.push(node);
node = node.parentNode;
}
ancestors.reverse();
return ancestors;
},
/**
* @param {string|Array=} [dimension='value'] Default 'value'. can be 0, 1, 2, 3
* @return {number} Value.
*/
getValue: function (dimension) {
var data = this.hostTree.data;
return data.get(data.getDimension(dimension || 'value'), this.dataIndex);
},
/**
* @param {Object} layout
* @param {boolean=} [merge=false]
*/
setLayout: function (layout, merge) {
this.dataIndex >= 0 && this.hostTree.data.setItemLayout(this.dataIndex, layout, merge);
},
/**
* @return {Object} layout
*/
getLayout: function () {
return this.hostTree.data.getItemLayout(this.dataIndex);
},
/**
* @param {string} [path]
* @return {module:echarts/model/Model}
*/
getModel: function (path) {
if (this.dataIndex < 0) {
return;
}
var hostTree = this.hostTree;
var itemModel = hostTree.data.getItemModel(this.dataIndex);
var levelModel = this.getLevelModel();
var leavesModel;
if (!levelModel && (this.children.length === 0 || this.children.length !== 0 && this.isExpand === false)) {
leavesModel = this.getLeavesModel();
}
return itemModel.getModel(path, (levelModel || leavesModel || hostTree.hostModel).getModel(path));
},
/**
* @return {module:echarts/model/Model}
*/
getLevelModel: function () {
return (this.hostTree.levelModels || [])[this.depth];
},
/**
* @return {module:echarts/model/Model}
*/
getLeavesModel: function () {
return this.hostTree.leavesModel;
},
/**
* @example
* setItemVisual('color', color);
* setItemVisual({
* 'color': color
* });
*/
setVisual: function (key, value) {
this.dataIndex >= 0 && this.hostTree.data.setItemVisual(this.dataIndex, key, value);
},
/**
* Get item visual
*/
getVisual: function (key, ignoreParent) {
return this.hostTree.data.getItemVisual(this.dataIndex, key, ignoreParent);
},
/**
* @public
* @return {number}
*/
getRawIndex: function () {
return this.hostTree.data.getRawIndex(this.dataIndex);
},
/**
* @public
* @return {string}
*/
getId: function () {
return this.hostTree.data.getId(this.dataIndex);
},
/**
* if this is an ancestor of another node
*
* @public
* @param {TreeNode} node another node
* @return {boolean} if is ancestor
*/
isAncestorOf: function (node) {
var parent = node.parentNode;
while (parent) {
if (parent === this) {
return true;
}
parent = parent.parentNode;
}
return false;
},
/**
* if this is an descendant of another node
*
* @public
* @param {TreeNode} node another node
* @return {boolean} if is descendant
*/
isDescendantOf: function (node) {
return node !== this && node.isAncestorOf(this);
}
};
/**
* @constructor
* @alias module:echarts/data/Tree
* @param {module:echarts/model/Model} hostModel
* @param {Array.<Object>} levelOptions
* @param {Object} leavesOption
*/
function Tree(hostModel, levelOptions, leavesOption) {
/**
* @type {module:echarts/data/Tree~TreeNode}
* @readOnly
*/
this.root;
/**
* @type {module:echarts/data/List}
* @readOnly
*/
this.data;
/**
* Index of each item is the same as the raw index of coresponding list item.
* @private
* @type {Array.<module:echarts/data/Tree~TreeNode}
*/
this._nodes = [];
/**
* @private
* @readOnly
* @type {module:echarts/model/Model}
*/
this.hostModel = hostModel;
/**
* @private
* @readOnly
* @type {Array.<module:echarts/model/Model}
*/
this.levelModels = zrUtil.map(levelOptions || [], function (levelDefine) {
return new Model(levelDefine, hostModel, hostModel.ecModel);
});
this.leavesModel = new Model(leavesOption || {}, hostModel, hostModel.ecModel);
}
Tree.prototype = {
constructor: Tree,
type: 'tree',
/**
* 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 {(Object|string)} options If string, means order.
* @param {string=} options.order 'preorder' or 'postorder'
* @param {string=} options.attr 'children' or 'viewChildren'
* @param {Function} cb
* @param {Object} [context]
*/
eachNode: function (options, cb, context) {
this.root.eachNode(options, cb, context);
},
/**
* @param {number} dataIndex
* @return {module:echarts/data/Tree~TreeNode}
*/
getNodeByDataIndex: function (dataIndex) {
var rawIndex = this.data.getRawIndex(dataIndex);
return this._nodes[rawIndex];
},
/**
* @param {string} name
* @return {module:echarts/data/Tree~TreeNode}
*/
getNodeByName: function (name) {
return this.root.getNodeByName(name);
},
/**
* Update item available by list,
* when list has been performed options like 'filterSelf' or 'map'.
*/
update: function () {
var data = this.data;
var nodes = this._nodes;
for (var i = 0, len = nodes.length; i < len; i++) {
nodes[i].dataIndex = -1;
}
for (var i = 0, len = data.count(); i < len; i++) {
nodes[data.getRawIndex(i)].dataIndex = i;
}
},
/**
* Clear all layouts
*/
clearLayouts: function () {
this.data.clearItemLayouts();
}
};
/**
* data node format:
* {
* name: ...
* value: ...
* children: [
* {
* name: ...
* value: ...
* children: ...
* },
* ...
* ]
* }
*
* @static
* @param {Object} dataRoot Root node.
* @param {module:echarts/model/Model} hostModel
* @param {Object} treeOptions
* @param {Array.<Object>} treeOptions.levels
* @param {Array.<Object>} treeOptions.leaves
* @return module:echarts/data/Tree
*/
Tree.createTree = function (dataRoot, hostModel, treeOptions) {
var tree = new Tree(hostModel, treeOptions.levels, treeOptions.leaves);
var listData = [];
var dimMax = 1;
buildHierarchy(dataRoot);
function buildHierarchy(dataNode, parentNode) {
var value = dataNode.value;
dimMax = Math.max(dimMax, zrUtil.isArray(value) ? value.length : 1);
listData.push(dataNode);
var node = new TreeNode(dataNode.name, tree);
parentNode ? addChild(node, parentNode) : tree.root = node;
tree._nodes.push(node);
var children = dataNode.children;
if (children) {
for (var i = 0; i < children.length; i++) {
buildHierarchy(children[i], node);
}
}
}
tree.root.updateDepthAndHeight(0);
var dimensionsInfo = createDimensions(listData, {
coordDimensions: ['value'],
dimensionsCount: dimMax
});
var list = new List(dimensionsInfo, hostModel);
list.initData(listData);
linkList({
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.
*
* @param {(module:echarts/data/Tree~TreeNode|Object)} child
*/
function addChild(child, node) {
var children = node.children;
if (child.parentNode === node) {
return;
}
children.push(child);
child.parentNode = node;
}
export default Tree;