/** * echarts图表类:力导向图 * * @author pissang (https://github.com/pissang/) * */ define(function (require) { 'use strict'; var ChartBase = require('./base'); var Graph = require('../data/Graph'); var ForceLayout = require('../layout/Force'); // 图形依赖 var LineShape = require('zrender/shape/Line'); var BezierCurveShape = require('zrender/shape/BezierCurve'); var ImageShape = require('zrender/shape/Image'); var IconShape = require('../util/shape/Icon'); var ecConfig = require('../config'); // 力导向布局图默认参数 ecConfig.force = { zlevel: 1, // 一级层叠 z: 2, // 二级层叠 // 布局中心 center: ['50%', '50%'], // 布局大小 size: '100%', // 防止节点和节点,节点和边之间的重叠 preventOverlap: false, // 布局冷却因子,值越小结束时间越短,值越大时间越长但是结果也越收敛 coolDown: 0.99, // 数据映射到圆的半径的最小值和最大值 minRadius: 10, maxRadius: 20, // 是否根据屏幕比例拉伸 ratioScaling: false, // 在 500+ 顶点的图上建议设置 large 为 true, 会使用 Barnes-Hut simulation // 同时开启 useWorker 并且把 steps 值调大 // 关于Barnes-Hut simulation: http://en.wikipedia.org/wiki/Barnes–Hut_simulation large: false, // 是否在浏览器支持 worker 的时候使用 web worker useWorker: false, // 每一帧 force 迭代的次数,仅在启用webworker的情况下有用 steps: 1, // 布局缩放因子,并不完全精确, 效果跟布局大小类似 scaling: 1.0, // 向心力因子,越大向心力越大( 所有顶点会往 center 的位置收拢 ) gravity: 1, symbol: 'circle', // symbolSize 为 0 的话使用映射到minRadius-maxRadius后的值 symbolSize: 0, linkSymbol: null, linkSymbolSize: [10, 15], draggable: true, clickable: true, roam: false, // 分类里如果有样式会覆盖节点默认样式 // categories: [{ // itemStyle // symbol // symbolSize // name // }], itemStyle: { normal: { // color: 各异, label: { show: false, position: 'inside' // textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE }, nodeStyle: { brushType : 'both', borderColor : '#5182ab', borderWidth: 1 }, linkStyle: { color: '#5182ab', width: 1, type: 'line' } }, emphasis: { // color: 各异, label: { show: false // textStyle: null // 默认使用全局文本样式,详见TEXTSTYLE }, nodeStyle: {}, linkStyle: { opacity: 0 } } } // nodes: [{ // name: 'xxx', // value: 1, // itemStyle: {}, // initial: [0, 0], // fixX: false, // fixY: false, // ignore: false, // symbol: 'circle', // symbolSize: 0 // }] // links: [{ // source: 1, // target: 2, // weight: 1, // itemStyle: {} // }, { // source: 'xxx', // target: 'ooo' // }] }; var ecData = require('../util/ecData'); var zrUtil = require('zrender/tool/util'); var zrConfig = require('zrender/config'); var vec2 = require('zrender/tool/vector'); /** * 构造函数 * @param {Object} messageCenter echart消息中心 * @param {ZRender} zr zrender实例 * @param {Object} series 数据 * @param {Object} component 组件 */ function Force(ecTheme, messageCenter, zr, option, myChart) { var self = this; // 图表基类 ChartBase.call(this, ecTheme, messageCenter, zr, option, myChart); // 保存节点的位置,改变数据时能够有更好的动画效果 this.__nodePositionMap = {}; this._graph = new Graph(true); this._layout = new ForceLayout(); this._layout.onupdate = function() { self._step(); }; this._steps = 1; // 关闭可拖拽属性 this.ondragstart = function() { ondragstart.apply(self, arguments); }; this.ondragend = function() { ondragend.apply(self, arguments); }; this.ondrop = function() {}; this.shapeHandler.ondragstart = function() { self.isDragstart = true; }; this.onmousemove = function() { onmousemove.apply(self, arguments); }; this.refresh(option); } /** * 绘制图形 */ Force.prototype = { constructor: Force, type : ecConfig.CHART_TYPE_FORCE, _init: function() { // var self = this; var legend = this.component.legend; var series = this.series; var serieName; this.clear(); for (var i = 0, l = series.length; i < l; i++) { var serie = series[i]; if (serie.type === ecConfig.CHART_TYPE_FORCE) { series[i] = this.reformOption(series[i]); serieName = series[i].name || ''; // 系列图例开关 this.selectedMap[serieName] = legend ? legend.isSelected(serieName) : true; if (!this.selectedMap[serieName]) { continue; } this.buildMark(i); // TODO 多个 force this._initSerie(serie, i); break; } } this.animationEffect(); }, _getNodeCategory: function (serie, node) { return serie.categories && serie.categories[node.category || 0]; }, _getNodeQueryTarget: function (serie, node, type) { type = type || 'normal'; var category = this._getNodeCategory(serie, node) || {}; return [ // Node node.itemStyle && node.itemStyle[type], // Category category && category.itemStyle && category.itemStyle[type], // Serie serie.itemStyle[type].nodeStyle ]; }, _getEdgeQueryTarget: function (serie, edge, type) { type = type || 'normal'; return [ (edge.itemStyle && edge.itemStyle[type]), serie.itemStyle[type].linkStyle ]; }, _initSerie: function(serie, serieIdx) { this._temperature = 1; // data-matrix 表示数据 if (serie.data) { this._graph = this._getSerieGraphFromDataMatrix(serie); } // node-links 表示数据 else { this._graph = this._getSerieGraphFromNodeLinks(serie); } this._buildLinkShapes(serie, serieIdx); this._buildNodeShapes(serie, serieIdx); var panable = serie.roam === true || serie.roam === 'move'; var zoomable = serie.roam === true || serie.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') ) { // 斗胆修改 EFFECT 层配置项 this.zr.modLayer(ecConfig.EFFECT_ZLEVEL, { panable: panable, zoomable: zoomable }); } this._initLayout(serie); this._step(); }, _getSerieGraphFromDataMatrix: function (serie) { var nodesData = []; var count = 0; var matrix = []; // 复制一份新的matrix for (var i = 0; i < serie.matrix.length; i++) { matrix[i] = serie.matrix[i].slice(); } var data = serie.data || serie.nodes; for (var i = 0; i < data.length; i++) { var node = {}; var group = data[i]; for (var key in group) { // name改为id if (key === 'name') { node['id'] = group['name']; } else { node[key] = group[key]; } } // legends 选择优先级 category -> group var category = this._getNodeCategory(serie, group); var name = category ? category.name : group.name; this.selectedMap[name] = this.isSelected(name); if (this.selectedMap[name]) { nodesData.push(node); count++; } else { // 过滤legend未选中的数据 matrix.splice(count, 1); for (var j = 0; j < matrix.length; j++) { matrix[j].splice(count, 1); } } } var graph = Graph.fromMatrix(nodesData, matrix, true); // Prepare layout parameters graph.eachNode(function (n, idx) { n.layout = { size: n.data.value, mass: 0 }; n.rawIndex = idx; }); graph.eachEdge(function (e) { e.layout = { weight: e.data.weight }; }); return graph; }, _getSerieGraphFromNodeLinks: function (serie) { var graph = new Graph(true); var nodes = serie.data || serie.nodes; for (var i = 0, len = nodes.length; i < len; i++) { var n = nodes[i]; if (!n || n.ignore) { continue; } // legends 选择优先级 category -> group var category = this._getNodeCategory(serie, n); var name = category ? category.name : n.name; this.selectedMap[name] = this.isSelected(name); if (this.selectedMap[name]) { var node = graph.addNode(n.name, n); node.rawIndex = i; } } for (var i = 0, len = serie.links.length; i < len; i++) { var e = serie.links[i]; var n1 = e.source; var n2 = e.target; if (typeof(n1) === 'number') { n1 = nodes[n1]; if (n1) { n1 = n1.name; } } if (typeof(n2) === 'number') { n2 = nodes[n2]; if (n2) { n2 = n2.name; } } var edge = graph.addEdge(n1, n2, e); if (edge) { edge.rawIndex = i; } } graph.eachNode(function (n) { var value = n.data.value; if (value == null) { // value 是 null 或者 undefined value = 0; // 默认使用所有边值的和作为节点的大小, 不修改 data 里的数值 for (var i = 0; i < n.edges.length; i++) { value += n.edges[i].data.weight || 0; } } n.layout = { size: value, mass: 0 }; }); graph.eachEdge(function (e) { e.layout = { // 默认 weight 为1 weight: e.data.weight == null ? 1 : e.data.weight }; }); return graph; }, _initLayout: function(serie) { var graph = this._graph; var len = graph.nodes.length; var minRadius = this.query(serie, 'minRadius'); var maxRadius = this.query(serie, 'maxRadius'); this._steps = serie.steps || 1; var layout = this._layout; layout.center = this.parseCenter(this.zr, serie.center); layout.width = this.parsePercent(serie.size, this.zr.getWidth()); layout.height = this.parsePercent(serie.size, this.zr.getHeight()); layout.large = serie.large; layout.scaling = serie.scaling; layout.ratioScaling = serie.ratioScaling; layout.gravity = serie.gravity; layout.temperature = 1; layout.coolDown = serie.coolDown; layout.preventNodeEdgeOverlap = serie.preventOverlap; layout.preventNodeOverlap = serie.preventOverlap; // 将值映射到minRadius-maxRadius的范围上 var min = Infinity; var max = -Infinity; for (var i = 0; i < len; i++) { var gNode = graph.nodes[i]; max = Math.max(gNode.layout.size, max); min = Math.min(gNode.layout.size, min); } var divider = max - min; for (var i = 0; i < len; i++) { var gNode = graph.nodes[i]; if (divider > 0) { gNode.layout.size = (gNode.layout.size - min) * (maxRadius - minRadius) / divider + minRadius; // 节点质量是归一的 gNode.layout.mass = gNode.layout.size / maxRadius; } else { gNode.layout.size = (maxRadius - minRadius) / 2; gNode.layout.mass = 0.5; } } for (var i = 0; i < len; i++) { // var initPos; var gNode = graph.nodes[i]; if (typeof(this.__nodePositionMap[gNode.id]) !== 'undefined') { gNode.layout.position = vec2.create(); vec2.copy(gNode.layout.position, this.__nodePositionMap[gNode.id]); } else if (typeof(gNode.data.initial) !== 'undefined') { gNode.layout.position = vec2.create(); vec2.copy(gNode.layout.position, gNode.data.initial); } else { var center = this._layout.center; var size = Math.min(this._layout.width, this._layout.height); gNode.layout.position = _randomInSquare( center[0], center[1], size * 0.8 ); } var style = gNode.shape.style; var radius = gNode.layout.size; style.width = style.width || (radius * 2); style.height = style.height || (radius * 2); style.x = -style.width / 2; style.y = -style.height / 2; vec2.copy(gNode.shape.position, gNode.layout.position); } // 边 len = graph.edges.length; max = -Infinity; for (var i = 0; i < len; i++) { var e = graph.edges[i]; if (e.layout.weight > max) { max = e.layout.weight; } } // 权重归一 for (var i = 0; i < len; i++) { var e = graph.edges[i]; e.layout.weight /= max; } this._layout.init(graph, serie.useWorker); }, _buildNodeShapes: function(serie, serieIdx) { var graph = this._graph; var categories = this.query(serie, 'categories'); graph.eachNode(function (node) { var category = this._getNodeCategory(serie, node.data); var queryTarget = [node.data, category, serie]; var styleQueryTarget = this._getNodeQueryTarget(serie, node.data); var emphasisStyleQueryTarget = this._getNodeQueryTarget( serie, node.data, 'emphasis' ); var shape = new IconShape({ style: { x: 0, y: 0, color: this.deepQuery(styleQueryTarget, 'color'), brushType: 'both', // 兼容原有写法 strokeColor: this.deepQuery(styleQueryTarget, 'strokeColor') || this.deepQuery(styleQueryTarget, 'borderColor'), lineWidth: this.deepQuery(styleQueryTarget, 'lineWidth') || this.deepQuery(styleQueryTarget, 'borderWidth') }, highlightStyle: { color: this.deepQuery(emphasisStyleQueryTarget, 'color'), // 兼容原有写法 strokeColor: this.deepQuery(emphasisStyleQueryTarget, 'strokeColor') || this.deepQuery(emphasisStyleQueryTarget, 'borderColor'), lineWidth: this.deepQuery(emphasisStyleQueryTarget, 'lineWidth') || this.deepQuery(emphasisStyleQueryTarget, 'borderWidth') }, clickable: serie.clickable, zlevel: this.getZlevelBase(), z: this.getZBase() }); if (!shape.style.color) { shape.style.color = category ? this.getColor(category.name) : this.getColor(node.id); } shape.style.iconType = this.deepQuery(queryTarget, 'symbol'); var symbolSize = this.deepQuery(queryTarget, 'symbolSize') || 0; if (typeof symbolSize === 'number') { symbolSize = [symbolSize, symbolSize]; } // 强制设定节点大小,否则默认映射到 minRadius 到 maxRadius 后的值 shape.style.width = symbolSize[0] * 2; shape.style.height = symbolSize[1] * 2; if (shape.style.iconType.match('image')) { shape.style.image = shape.style.iconType.replace( new RegExp('^image:\\/\\/'), '' ); shape = new ImageShape({ 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 = node.data.label == null ? node.id : node.data.label; shape.style.textPosition = this.deepQuery( queryTarget, 'itemStyle.normal.label.position' ) ; 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' ) || {}); } // 拖拽特性 if (this.deepQuery(queryTarget, 'draggable')) { this.setCalculable(shape); shape.dragEnableTime = 0; shape.draggable = true; shape.ondragstart = this.shapeHandler.ondragstart; shape.ondragover = null; } var categoryName = ''; if (typeof(node.category) !== 'undefined') { var category = categories[node.category]; categoryName = (category && category.name) || ''; } // !!Pack data before addShape ecData.pack( shape, serie, serieIdx, // data node.data, // data index node.rawIndex, // name node.data.name || '', // category // special node.category ); this.shapeList.push(shape); this.zr.addShape(shape); node.shape = shape; }, this); }, _buildLinkShapes: function(serie, serieIdx) { var graph = this._graph; var len = graph.edges.length; for (var i = 0; i < len; i++) { var gEdge = graph.edges[i]; var link = gEdge.data; var source = gEdge.node1; var target = gEdge.node2; var otherEdge = graph.getEdge(target, source); var queryTarget = this._getEdgeQueryTarget(serie, link); var linkType = this.deepQuery(queryTarget, 'type'); // TODO 暂时只有线段支持箭头 if (serie.linkSymbol && serie.linkSymbol !== 'none') { linkType = 'line'; } var LinkShapeCtor = linkType === 'line' ? LineShape : BezierCurveShape; var linkShape = new LinkShapeCtor({ style : { xStart : 0, yStart : 0, xEnd : 0, yEnd : 0 }, clickable: this.query(serie, 'clickable'), highlightStyle : {}, zlevel: this.getZlevelBase(), z: this.getZBase() }); if (otherEdge && otherEdge.shape) { // 偏移一定位置放置双向边重叠 linkShape.style.offset = 4; otherEdge.shape.style.offset = 4; } zrUtil.merge( linkShape.style, this.query(serie, 'itemStyle.normal.linkStyle'), true ); zrUtil.merge( linkShape.highlightStyle, this.query(serie, 'itemStyle.emphasis.linkStyle'), true ); if (typeof(link.itemStyle) !== 'undefined') { if(link.itemStyle.normal){ zrUtil.merge(linkShape.style, link.itemStyle.normal, true); } if(link.itemStyle.emphasis){ zrUtil.merge( linkShape.highlightStyle, link.itemStyle.emphasis, true ); } } // 兼容原有写法 linkShape.style.lineWidth = linkShape.style.lineWidth || linkShape.style.width; linkShape.style.strokeColor = linkShape.style.strokeColor || linkShape.style.color; linkShape.highlightStyle.lineWidth = linkShape.highlightStyle.lineWidth || linkShape.highlightStyle.width; linkShape.highlightStyle.strokeColor = linkShape.highlightStyle.strokeColor || linkShape.highlightStyle.color; ecData.pack( linkShape, // serie serie, // serie index serieIdx, // link data gEdge.data, // link data index gEdge.rawIndex == null ? i : gEdge.rawIndex, // source name - target name gEdge.data.name || (source.id + ' - ' + target.id), // link source id // special source.id, // link target id // special2 target.id ); this.shapeList.push(linkShape); this.zr.addShape(linkShape); gEdge.shape = linkShape; // Arrow shape if (serie.linkSymbol && serie.linkSymbol !== 'none') { var symbolShape = new IconShape({ style: { x: -5, y: 0, width: serie.linkSymbolSize[0], height: serie.linkSymbolSize[1], iconType: serie.linkSymbol, brushType: 'fill', // Use same style with link shape color: linkShape.style.strokeColor }, highlightStyle: { brushType: 'fill' }, position: [0, 0], rotation: 0 }); linkShape._symbolShape = symbolShape; this.shapeList.push(symbolShape); this.zr.addShape(symbolShape); } } }, _updateLinkShapes: function() { var v = vec2.create(); var n = vec2.create(); var p1 = vec2.create(); var p2 = vec2.create(); var edges = this._graph.edges; for (var i = 0, len = edges.length; i < len; i++) { var edge = edges[i]; var sourceShape = edge.node1.shape; var targetShape = edge.node2.shape; vec2.copy(p1, sourceShape.position); vec2.copy(p2, targetShape.position); var edgeShapeStyle = edge.shape.style; vec2.sub(v, p1, p2); vec2.normalize(v, v); if (edgeShapeStyle.offset) { n[0] = v[1]; n[1] = - v[0]; vec2.scaleAndAdd(p1, p1, n, edgeShapeStyle.offset); vec2.scaleAndAdd(p2, p2, n, edgeShapeStyle.offset); } else if (edge.shape.type === 'bezier-curve') { edgeShapeStyle.cpX1 = (p1[0] + p2[0]) / 2 - (p2[1] - p1[1]) / 4; edgeShapeStyle.cpY1 = (p1[1] + p2[1]) / 2 - (p1[0] - p2[0]) / 4; } edgeShapeStyle.xStart = p1[0]; edgeShapeStyle.yStart = p1[1]; edgeShapeStyle.xEnd = p2[0]; edgeShapeStyle.yEnd = p2[1]; edge.shape.modSelf(); if (edge.shape._symbolShape) { var symbolShape = edge.shape._symbolShape; vec2.copy(symbolShape.position, p2); vec2.scaleAndAdd( symbolShape.position, symbolShape.position, v, targetShape.style.width / 2 + 2 ); var angle = Math.atan2(v[1], v[0]); symbolShape.rotation = Math.PI / 2 - angle; symbolShape.modSelf(); } } }, _syncNodePositions: function() { var graph = this._graph; for (var i = 0; i < graph.nodes.length; i++) { var gNode = graph.nodes[i]; var position = gNode.layout.position; var node = gNode.data; var shape = gNode.shape; var fixX = shape.fixed || node.fixX; var fixY = shape.fixed || node.fixY; if (fixX === true) { fixX = 1; } else if (isNaN(fixX)) { fixX = 0; } if (fixY === true) { fixY = 1; } else if (isNaN(fixY)) { fixY = 0; } shape.position[0] += (position[0] - shape.position[0]) * (1 - fixX); shape.position[1] += (position[1] - shape.position[1]) * (1 - fixY); vec2.copy(position, shape.position); var nodeName = node.name; if (nodeName) { var gPos = this.__nodePositionMap[nodeName]; if (!gPos) { gPos = this.__nodePositionMap[nodeName] = vec2.create(); } vec2.copy(gPos, position); } shape.modSelf(); } }, _step: function(e) { this._syncNodePositions(); this._updateLinkShapes(); this.zr.refreshNextFrame(); if (this._layout.temperature > 0.01) { this._layout.step(this._steps); } else { this.messageCenter.dispatch( ecConfig.EVENT.FORCE_LAYOUT_END, {}, {}, this.myChart ); } }, refresh: function(newOption) { if (newOption) { this.option = newOption; this.series = this.option.series; } this.legend = this.component.legend; if (this.legend) { this.getColor = function(param) { return this.legend.getColor(param); }; this.isSelected = function(param) { return this.legend.isSelected(param); }; } else { var colorMap = {}; var count = 0; this.getColor = function (key) { if (colorMap[key]) { return colorMap[key]; } if (!colorMap[key]) { colorMap[key] = this.zr.getColor(count++); } return colorMap[key]; }; this.isSelected = function () { return true; }; } this._init(); }, dispose: function(){ this.clear(); this.shapeList = null; this.effectList = null; this._layout.dispose(); this._layout = null; this.__nodePositionMap = {}; }, getPosition: function () { var position = []; this._graph.eachNode(function (n) { if (n.layout) { position.push({ name: n.data.name, position: Array.prototype.slice.call(n.layout.position) }); } }); return position; } }; /** * 拖拽开始 */ function ondragstart(param) { if (!this.isDragstart || !param.target) { // 没有在当前实例上发生拖拽行为则直接返回 return; } var shape = param.target; shape.fixed = true; // 处理完拖拽事件后复位 this.isDragstart = false; this.zr.on(zrConfig.EVENT.MOUSEMOVE, this.onmousemove); } function onmousemove() { this._layout.temperature = 0.8; this._step(); } /** * 数据项被拖拽出去,重载基类方法 */ function ondragend(param, status) { if (!this.isDragend || !param.target) { // 没有在当前实例上发生拖拽行为则直接返回 return; } var shape = param.target; shape.fixed = false; // 别status = {}赋值啊!! status.dragIn = true; //你自己refresh的话把他设为false,设true就会重新调refresh接口 status.needRefresh = false; // 处理完拖拽事件后复位 this.isDragend = false; this.zr.un(zrConfig.EVENT.MOUSEMOVE, this.onmousemove); } function _randomInSquare(x, y, size) { var v = vec2.create(); v[0] = (Math.random() - 0.5) * size + x; v[1] = (Math.random() - 0.5) * size + y; return v; } zrUtil.inherits(Force, ChartBase); // 图表注册 require('../chart').define('force', Force); return Force; });