/** * echarts图表类:树图 * * @desc echarts基于Canvas,纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据统计图表。 * @author Loutongbing (娄同兵, loutongbing@126.com) */ define(function (require) { var ChartBase = require('./base'); var GOLDEN_SECTION = 0.618; // 图形依赖 var IconShape = require('../util/shape/Icon'); var ImageShape = require('zrender/shape/Image'); var LineShape = require('zrender/shape/Line'); var BezierCurveShape = require('zrender/shape/BezierCurve'); // 布局依赖 var TreeLayout = require('../layout/Tree'); // 数据依赖 var TreeData = require('../data/Tree'); var ecConfig = require('../config'); // 默认参数 ecConfig.tree = { zlevel: 1, // 一级层叠 z: 2, // 二级层叠 calculable: false, clickable: true, rootLocation: {}, orient: 'vertical', symbol: 'circle', symbolSize: 20, nodePadding: 30, layerPadding: 100, /*rootLocation: { x: 'center' | 'left' | 'right' | 'x%' | {number}, y: 'center' | 'top' | 'bottom' | 'y%' | {number} },*/ itemStyle: { normal: { // color: 各异, label: { show: true }, lineStyle: { width: 1, color: '#777', type: 'curve' // curve } }, emphasis: { } } }; var ecData = require('../util/ecData'); var zrConfig = require('zrender/config'); var zrEvent = require('zrender/tool/event'); var zrUtil = require('zrender/tool/util'); /** * 构造函数 * @param {Object} messageCenter echart消息中心 * @param {ZRender} zr zrender实例 * @param {Object} series 数据 * @param {Object} component 组件 * @constructor * @exports Tree */ function Tree(ecTheme, messageCenter, zr, option, myChart) { // 图表基类 ChartBase.call(this, ecTheme, messageCenter, zr, option, myChart); this.refresh(option); } Tree.prototype = { type : ecConfig.CHART_TYPE_TREE, /** * 构建单个 * * @param {Object} data 数据 */ _buildShape : function (series, seriesIndex) { var data = series.data[0]; this.tree = TreeData.fromOptionData(data.name, data.children); // 添加root的data this.tree.root.data = data; // 根据root坐标 方向 对每个节点的坐标进行映射 this._setTreeShape(series); // 递归画出树节点与连接线 this.tree.traverse( function (treeNode) { this._buildItem( treeNode, series, seriesIndex ); // 画连接线 if (treeNode.children.length > 0) { this._buildLink( treeNode, series ); } }, this ); var panable = series.roam === true || series.roam === 'move'; var zoomable = series.roam === true || series.roam === 'scale'; // Enable pan and zooom this.zr.modLayer(this.getZlevelBase(), { panable: panable, zoomable: zoomable }); if ( this.query('markPoint.effect.show') || this.query('markLine.effect.show') ) { this.zr.modLayer(ecConfig.EFFECT_ZLEVEL, { panable: panable, zoomable: zoomable }); } this.addShapeList(); }, /** * 构建单个item */ _buildItem : function ( treeNode, serie, seriesIndex ) { var queryTarget = [treeNode.data, serie]; var symbol = this.deepQuery(queryTarget, 'symbol'); // 多级控制 var normal = this.deepMerge( queryTarget, 'itemStyle.normal' ) || {}; var emphasis = this.deepMerge( queryTarget, 'itemStyle.emphasis' ) || {}; var normalColor = normal.color || this.zr.getColor(); var emphasisColor = emphasis.color || this.zr.getColor(); var angle = -treeNode.layout.angle || 0; // 根节点不旋转 if (treeNode.id === this.tree.root.id) { angle = 0; } var textPosition = 'right'; if (Math.abs(angle) >= Math.PI / 2 && Math.abs(angle) < Math.PI * 3 / 2) { angle += Math.PI; textPosition = 'left'; } var rotation = [ angle, treeNode.layout.position[0], treeNode.layout.position[1] ]; var shape = new IconShape({ zlevel: this.getZlevelBase(), z: this.getZBase() + 1, rotation: rotation, clickable: this.deepQuery(queryTarget, 'clickable'), style: { x: treeNode.layout.position[0] - treeNode.layout.width * 0.5, y: treeNode.layout.position[1] - treeNode.layout.height * 0.5, width: treeNode.layout.width, height: treeNode.layout.height, iconType: symbol, color: normalColor, brushType: 'both', lineWidth: normal.borderWidth, strokeColor: normal.borderColor }, highlightStyle: { color: emphasisColor, lineWidth: emphasis.borderWidth, strokeColor: emphasis.borderColor } }); if (shape.style.iconType.match('image')) { shape.style.image = shape.style.iconType.replace( new RegExp('^image:\\/\\/'), '' ); shape = new ImageShape({ rotation: rotation, style: shape.style, highlightStyle: shape.highlightStyle, clickable: shape.clickable, zlevel: this.getZlevelBase(), z: this.getZBase() }); } // 节点标签样式 if (this.deepQuery(queryTarget, 'itemStyle.normal.label.show')) { shape.style.text = treeNode.data.label == null ? treeNode.id : treeNode.data.label; shape.style.textPosition = this.deepQuery( queryTarget, 'itemStyle.normal.label.position' ); // 极坐标另外计算 时钟哪个侧面 if (serie.orient === 'radial' && shape.style.textPosition !== 'inside') { shape.style.textPosition = textPosition; } shape.style.textColor = this.deepQuery( queryTarget, 'itemStyle.normal.label.textStyle.color' ); shape.style.textFont = this.getFont(this.deepQuery( queryTarget, 'itemStyle.normal.label.textStyle' ) || {}); } if (this.deepQuery(queryTarget, 'itemStyle.emphasis.label.show')) { shape.highlightStyle.textPosition = this.deepQuery( queryTarget, 'itemStyle.emphasis.label.position' ); shape.highlightStyle.textColor = this.deepQuery( queryTarget, 'itemStyle.emphasis.label.textStyle.color' ); shape.highlightStyle.textFont = this.getFont(this.deepQuery( queryTarget, 'itemStyle.emphasis.label.textStyle' ) || {}); } // todo ecData.pack( shape, serie, seriesIndex, treeNode.data, 0, treeNode.id ); this.shapeList.push(shape); }, _buildLink : function ( parentNode, serie ) { var lineStyle = serie.itemStyle.normal.lineStyle; // 折线另外计算 if (lineStyle.type === 'broken') { this._buildBrokenLine( parentNode, lineStyle, serie ); return; } for (var i = 0; i < parentNode.children.length; i++) { var xStart = parentNode.layout.position[0]; var yStart = parentNode.layout.position[1]; var xEnd = parentNode.children[i].layout.position[0]; var yEnd = parentNode.children[i].layout.position[1]; switch (lineStyle.type) { case 'curve': this._buildBezierCurve( parentNode, parentNode.children[i], lineStyle, serie ); break; // 折线 case 'broken': break; // default画直线 default: var shape = this._getLine( xStart, yStart, xEnd, yEnd, lineStyle ); this.shapeList.push(shape); } } }, _buildBrokenLine: function ( parentNode, lineStyle, serie ) { // 引用_getLine需要把type改为solid var solidLineStyle = zrUtil.clone(lineStyle); solidLineStyle.type = 'solid'; var shapes = []; var xStart = parentNode.layout.position[0]; var yStart = parentNode.layout.position[1]; var orient = serie.orient; // 子节点的y var yEnd = parentNode.children[0].layout.position[1]; // 中点x y var xMiddle = xStart; var yMiddle = yStart + (yEnd - yStart) * (1 - GOLDEN_SECTION); // 中线的起始 var xMiddleStart = parentNode.children[0].layout.position[0]; var yMiddleStart = yMiddle; var xMiddleEnd = parentNode.children[parentNode.children.length - 1].layout.position[0]; var yMiddleEnd = yMiddle; // 水平状态 if (orient === 'horizontal') { var xEnd = parentNode.children[0].layout.position[0]; xMiddle = xStart + (xEnd - xStart) * (1 - GOLDEN_SECTION); yMiddle = yStart; xMiddleStart = xMiddle; yMiddleStart = parentNode.children[0].layout.position[1]; xMiddleEnd = xMiddle; yMiddleEnd = parentNode.children[parentNode.children.length - 1].layout.position[1]; } // 第一条 从根节点垂直向下 shapes.push( this._getLine( xStart, yStart, xMiddle, yMiddle, solidLineStyle ) ); // 第二条 横向 shapes.push( this._getLine( xMiddleStart, yMiddleStart, xMiddleEnd, yMiddleEnd, solidLineStyle ) ); // 第三条 垂直向下到子节点 for (var i = 0; i < parentNode.children.length; i++) { xEnd = parentNode.children[i].layout.position[0]; yEnd = parentNode.children[i].layout.position[1]; // 水平状态 if (orient === 'horizontal') { yMiddleStart = yEnd; } else { xMiddleStart = xEnd; } shapes.push( this._getLine( xMiddleStart, yMiddleStart, xEnd, yEnd, solidLineStyle ) ); } this.shapeList = this.shapeList.concat(shapes); }, _getLine: function ( xStart, yStart, xEnd, yEnd, lineStyle ) { if (xStart === xEnd) { xStart = xEnd = this.subPixelOptimize(xStart, lineStyle.width); } if (yStart === yEnd) { yStart = yEnd = this.subPixelOptimize(yStart, lineStyle.width); } return new LineShape({ zlevel: this.getZlevelBase(), hoverable: false, style: zrUtil.merge( { xStart: xStart, yStart: yStart, xEnd: xEnd, yEnd: yEnd, lineType: lineStyle.type, strokeColor: lineStyle.color, lineWidth: lineStyle.width }, lineStyle, true ) }); }, _buildBezierCurve: function ( parentNode, treeNode, lineStyle, serie ) { var offsetRatio = GOLDEN_SECTION; var orient = serie.orient; var xStart = parentNode.layout.position[0]; var yStart = parentNode.layout.position[1]; var xEnd = treeNode.layout.position[0]; var yEnd = treeNode.layout.position[1]; var cpX1 = xStart; var cpY1 = (yEnd - yStart) * offsetRatio + yStart; var cpX2 = xEnd; var cpY2 = (yEnd - yStart) * (1 - offsetRatio) + yStart; if (orient === 'horizontal') { cpX1 = (xEnd - xStart) * offsetRatio + xStart; cpY1 = yStart; cpX2 = (xEnd - xStart) * (1 - offsetRatio) + xStart; cpY2 = yEnd; } else if (orient === 'radial') { // 根节点 画直线 if (parentNode.id === this.tree.root.id) { cpX1 = (xEnd - xStart) * offsetRatio + xStart; cpY1 = (yEnd - yStart) * offsetRatio + yStart; cpX2 = (xEnd - xStart) * (1 - offsetRatio) + xStart; cpY2 = (yEnd - yStart) * (1 - offsetRatio) + yStart; } else { var xStartOrigin = parentNode.layout.originPosition[0]; var yStartOrigin = parentNode.layout.originPosition[1]; var xEndOrigin = treeNode.layout.originPosition[0]; var yEndOrigin = treeNode.layout.originPosition[1]; var rootX = this.tree.root.layout.position[0]; var rootY = this.tree.root.layout.position[1]; cpX1 = xStartOrigin; cpY1 = (yEndOrigin - yStartOrigin) * offsetRatio + yStartOrigin; cpX2 = xEndOrigin; cpY2 = (yEndOrigin - yStartOrigin) * (1 - offsetRatio) + yStartOrigin; var rad = (cpX1 - this.minX) / this.width * Math.PI * 2; cpX1 = cpY1 * Math.cos(rad) + rootX; cpY1 = cpY1 * Math.sin(rad) + rootY; rad = (cpX2 - this.minX) / this.width * Math.PI * 2; cpX2 = cpY2 * Math.cos(rad) + rootX; cpY2 = cpY2 * Math.sin(rad) + rootY; } } var shape = new BezierCurveShape({ zlevel: this.getZlevelBase(), hoverable: false, style: zrUtil.merge( { xStart: xStart, yStart: yStart, cpX1: cpX1, cpY1: cpY1, cpX2: cpX2, cpY2: cpY2, xEnd: xEnd, yEnd: yEnd, strokeColor: lineStyle.color, lineWidth: lineStyle.width }, lineStyle, true ) }); this.shapeList.push(shape); }, _setTreeShape : function (serie) { // 跑出来树的layout var treeLayout = new TreeLayout( { nodePadding: serie.nodePadding, layerPadding: serie.layerPadding } ); this.tree.traverse( function (treeNode) { var queryTarget = [treeNode.data, serie]; var symbolSize = this.deepQuery(queryTarget, 'symbolSize'); if (typeof symbolSize === 'number') { symbolSize = [symbolSize, symbolSize]; } treeNode.layout = { width: symbolSize[0], // 节点大小 height: symbolSize[1] }; }, this ); treeLayout.run(this.tree); // 树的方向 var orient = serie.orient; var rootX = serie.rootLocation.x; var rootY = serie.rootLocation.y; var zrWidth = this.zr.getWidth(); var zrHeight = this.zr.getHeight(); if (rootX === 'center') { rootX = zrWidth * 0.5; } else { rootX = this.parsePercent(rootX, zrWidth); } if (rootY === 'center') { rootY = zrHeight * 0.5; } else { rootY = this.parsePercent(rootY, zrHeight); } rootY = this.parsePercent(rootY, zrHeight); // 水平树 if (orient === 'horizontal') { rootX = isNaN(rootX) ? 10 : rootX; rootY = isNaN(rootY) ? zrHeight * 0.5 : rootY; } // 极坐标 if (orient === 'radial') { rootX = isNaN(rootX) ? zrWidth * 0.5 : rootX; rootY = isNaN(rootY) ? zrHeight * 0.5 : rootY; } // 纵向树 else { rootX = isNaN(rootX) ? zrWidth * 0.5 : rootX; rootY = isNaN(rootY) ? 10 : rootY; } // tree layout自动算出来的root的坐标 var originRootX = this.tree.root.layout.position[0]; // 若是极坐标,则求最大最小值 if (orient === 'radial') { var minX = Infinity; var maxX = 0; var maxWidth = 0; this.tree.traverse( function (treeNode) { maxX = Math.max(maxX, treeNode.layout.position[0]); minX = Math.min(minX, treeNode.layout.position[0]); maxWidth = Math.max(maxWidth, treeNode.layout.width); } ); // 2 * maxWidth 还不大好但至少保证绝不会重叠 todo this.width = maxX - minX + 2 * maxWidth; this.minX = minX; } this.tree.traverse( function (treeNode) { var x; var y; if (orient === 'vertical' && serie.direction === 'inverse') { x = treeNode.layout.position[0] - originRootX + rootX; y = rootY - treeNode.layout.position[1]; } else if (orient === 'vertical') { x = treeNode.layout.position[0] - originRootX + rootX; y = treeNode.layout.position[1] + rootY; } else if (orient === 'horizontal' && serie.direction === 'inverse') { y = treeNode.layout.position[0] - originRootX + rootY; x = rootX - treeNode.layout.position[1]; } else if (orient === 'horizontal') { y = treeNode.layout.position[0] - originRootX + rootY; x = treeNode.layout.position[1] + rootX; } // 极坐标 else { x = treeNode.layout.position[0]; y = treeNode.layout.position[1]; // 记录原始坐标,以后计算贝塞尔曲线的控制点 treeNode.layout.originPosition = [x, y]; var r = y; var angle = (x - minX) / this.width * Math.PI * 2; x = r * Math.cos(angle) + rootX; y = r * Math.sin(angle) + rootY; treeNode.layout.angle = angle; } treeNode.layout.position[0] = x; treeNode.layout.position[1] = y; }, this ); }, /* * 刷新 */ /* refresh: function (newOption) { this.clear(); if (newOption) { this.option = newOption; this.series = newOption.series; } this._buildShape(); }*/ refresh: function (newOption) { this.clear(); if (newOption) { this.option = newOption; this.series = this.option.series; } // Map storing all trees of series var series = this.series; var legend = this.component.legend; for (var i = 0; i < series.length; i++) { if (series[i].type === ecConfig.CHART_TYPE_TREE) { series[i] = this.reformOption(series[i]); var seriesName = series[i].name || ''; this.selectedMap[seriesName] = legend ? legend.isSelected(seriesName) : true; if (!this.selectedMap[seriesName]) { continue; } this._buildSeries(series[i], i); } } }, _buildSeries: function (series, seriesIndex) { /*var tree = Tree.fromOptionData('root', series.data); this._treesMap[seriesIndex] = tree;*/ // this._buildTreemap(tree.root, seriesIndex); this._buildShape(series, seriesIndex); } }; zrUtil.inherits(Tree, ChartBase); // 图表注册 require('../chart').define('tree', Tree); return Tree; });