blob: b25907991a3291d4b76bcd20d550701d100d637f [file] [log] [blame]
/**
* echarts图表类:力导向图
*
* @author pissang (https://github.com/pissang/)
*
*/
define(function (require) {
'use strict';
var ComponentBase = require('../component/base');
var ChartBase = require('./base');
var ForceLayout = require('./ForceLayoutWorker');
// 图形依赖
var LineShape = require('zrender/shape/Line');
var IconShape = require('../util/shape/Icon');
var ecConfig = require('../config');
var ecData = require('../util/ecData');
var zrUtil = require('zrender/tool/util');
var zrConfig = require('zrender/config');
var vec2 = require('zrender/tool/vector');
var NDArray = require('../util/ndarray');
var ArrayCtor = typeof(Float32Array) == 'undefined' ? Array : Float32Array;
var requestAnimationFrame = window.requestAnimationFrame
|| window.msRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.webkitRequestAnimationFrame
|| function (func){setTimeout(func, 16);};
// Use inline web worker
var workerUrl;
if (
typeof(Worker) !== 'undefined' &&
typeof(Blob) !== 'undefined'
) {
var blob = new Blob([ForceLayout.getWorkerCode()]);
workerUrl = window.URL.createObjectURL(blob);
}
function getToken() {
return Math.round(new Date().getTime() / 100) % 10000000;
}
/**
* 构造函数
* @param {Object} messageCenter echart消息中心
* @param {ZRender} zr zrender实例
* @param {Object} series 数据
* @param {Object} component 组件
*/
function Force(ecTheme, messageCenter, zr, option, myChart) {
var self = this;
// 基类
ComponentBase.call(this, ecTheme, messageCenter, zr, option, myChart);
// 图表基类
ChartBase.call(this);
// 保存节点的位置,改变数据时能够有更好的动画效果
// TODO
this.__nodePositionMap = {};
this._nodeShapes = [];
this._linkShapes = [];
this._updating = true;
this._filteredNodes = null;
this._filteredLinks = null;
this._rawNodes = null;
this._rawLinks = null;
this._steps = 1;
this._coolDown = 0.99;
// 关闭可拖拽属性
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._init();
}
/**
* 绘制图形
*/
Force.prototype = {
constructor: Force,
type : ecConfig.CHART_TYPE_FORCE,
_init: function() {
var self = this;
this.clear();
this._updating = true;
this._buildShape();
if (this._layoutWorker) {
this._layoutWorker.onmessage = function(e) {
if (self._temperature < 0.01) {
requestAnimationFrame(function() {
self._step.call(self, e);
});
} else {
self._step.call(self, e);
}
};
this._layoutWorker.postMessage({
cmd: 'update',
steps: this._steps,
temperature: this._temperature,
coolDown: this._coolDown
});
}
else {
var cb = function() {
if (self._updating) {
self._step();
requestAnimationFrame(cb);
}
};
requestAnimationFrame(cb);
}
},
_buildShape: function() {
var legend = this.component.legend;
var series = this.series;
var serieName;
this._temperature = 1;
this.shapeList.length = 0;
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 || '';
if (workerUrl && serie.useWorker) {
try {
if (!this._layoutWorker) {
this._layoutWorker = new Worker(workerUrl);
}
this._layout = null;
} catch(e) { // IE10-11 will throw security error when using blog url
this._layoutWorker = null;
if (!this._layout) {
this._layout = new ForceLayout();
}
}
} else {
if (!this._layout) {
this._layout = new ForceLayout();
}
if (this._layoutWorker) {
this._layoutWorker.terminate();
this._layoutWorker = null;
}
}
// 系列图例开关
this.selectedMap[serieName] =
legend ? legend.isSelected(serieName) : true;
if (!this.selectedMap[serieName]) {
continue;
}
this.buildMark(i);
// 同步selected状态
var categories = serie.categories;
for (var j = 0, len = categories.length; j < len; j++) {
if (categories[j].name) {
if (legend){
this.selectedMap[j] =
legend.isSelected(categories[j].name);
} else {
this.selectedMap[j] = true;
}
}
}
this._preProcessData(serie);
this._nodeShapes.length = 0;
this._linkShapes.length = 0;
this._buildLinkShapes(serie);
this._buildNodeShapes(serie);
this._initLayout(serie);
this._updateLinkShapes();
// TODO 多个 force
this._forceSerie = serie;
break;
}
}
},
_preProcessData: function(serie) {
this._rawNodes = this.query(serie, 'nodes');
this._rawLinks = zrUtil.clone(this.query(serie, 'links'));
var filteredNodeList = [];
var filteredNodeMap = {};
var cursor = 0;
var self = this;
this._filteredNodes = _filter(this._rawNodes, function (node, i) {
if (!node) {
return;
}
if (node.ignore) {
return;
}
var idx = -1;
if (
typeof(node.category) == 'undefined'
|| self.selectedMap[node.category]
) {
idx = cursor++;
}
if (node.name) {
filteredNodeMap[node.name] = idx;
}
filteredNodeList[i] = idx;
return idx >= 0;
});
var source;
var target;
this._filteredLinks = _filter(this._rawLinks, function (link, i){
source = link.source;
target = link.target;
var ret = true;
var idx = typeof(source) === 'string'
? filteredNodeMap[source] // source 用 node id 表示
: filteredNodeList[source]; // source 用 node index 表示
if (typeof(idx) == 'undefined') {
idx = -1;
}
if (idx >= 0) {
link.source = idx;
} else {
ret = false;
}
var idx = typeof(target) === 'string'
? filteredNodeMap[target] // target 用 node id 表示
: filteredNodeList[target]; // target 用 node index 表示
if (typeof(idx) == 'undefined') {
idx = -1;
}
if (idx >= 0) {
link.target = idx;
} else {
ret = false;
}
// 保存原始链接中的index
link.rawIndex = i;
return ret;
});
},
_initLayout: function(serie) {
var nodes = this._filteredNodes;
var links = this._filteredLinks;
var shapes = this._nodeShapes;
var len = nodes.length;
var minRadius = this.query(serie, 'minRadius');
var maxRadius = this.query(serie, 'maxRadius');
this._steps = serie.steps || 1;
this._coolDown = serie.coolDown || 0.99;
var center = this.parseCenter(this.zr, serie.center);
var width = this.parsePercent(serie.size, this.zr.getWidth());
var height = this.parsePercent(serie.size, this.zr.getHeight());
var size = Math.min(width, height);
// 将值映射到minRadius-maxRadius的范围上
var radius = [];
for (var i = 0; i < len; i++) {
var node = nodes[i];
radius.push(node.value || 1);
}
var arr = new NDArray(radius);
radius = arr.map(minRadius, maxRadius).toArray();
var max = arr.max();
if (max === 0) {
return;
}
var massArr = arr.mul(1/max, arr).toArray();
var positionArr = new ArrayCtor(len * 2);
for (var i = 0; i < len; i++) {
var initPos;
var node = nodes[i];
if (typeof(this.__nodePositionMap[node.name]) !== 'undefined') {
initPos = vec2.create();
vec2.copy(initPos, this.__nodePositionMap[node.name]);
} else if (typeof(node.initial) !== 'undefined') {
initPos = Array.prototype.slice.call(node.initial);
} else {
initPos = _randomInSquare(
center[0], center[1], size * 0.8
);
}
var style = shapes[i].style;
style.width = style.width || (radius[i] * 2);
style.height = style.height || (radius[i] * 2);
style.x = -style.width / 2;
style.y = -style.height / 2;
shapes[i].position = initPos;
positionArr[i * 2] = initPos[0];
positionArr[i * 2 + 1] = initPos[1];
}
len = links.length;
var edgeArr = new ArrayCtor(len * 2);
var edgeWeightArr = new ArrayCtor(len);
for (var i = 0; i < len; i++) {
var link = links[i];
edgeArr[i * 2] = link.source;
edgeArr[i * 2 + 1] = link.target;
edgeWeightArr[i] = link.weight || 1;
}
arr = new NDArray(edgeWeightArr);
var max = arr.max();
if (max === 0) {
return;
}
var edgeWeightArr = arr.mul(1 / max, arr)._array;
var config = {
center: center,
width: serie.ratioScaling ? width : size,
height: serie.ratioScaling ? height : size,
scaling: serie.scaling || 1.0,
gravity: serie.gravity || 1.0,
barnesHutOptimize: serie.large
};
if (this._layoutWorker) {
this._token = getToken();
this._layoutWorker.postMessage({
cmd: 'init',
nodesPosition: positionArr,
nodesMass: massArr,
nodesSize: radius,
edges: edgeArr,
edgesWeight: edgeWeightArr,
token: this._token
});
this._layoutWorker.postMessage({
cmd: 'updateConfig',
config: config
});
} else {
zrUtil.merge(this._layout, config, true);
this._layout.initNodes(positionArr, massArr, radius);
this._layout.initEdges(edgeArr, edgeWeightArr);
}
},
_buildNodeShapes: function(serie) {
var categories = this.query(serie, 'categories');
var nodes = this._filteredNodes;
var len = nodes.length;
var legend = this.component.legend;
for (var i = 0; i < len; i++) {
var node = nodes[i];
var shape = new IconShape({
style : {
x : 0,
y : 0
},
clickable : true,
highlightStyle : {}
});
var queryTarget = [];
var shapeNormalStyle = [];
var shapeEmphasisStyle = [];
queryTarget.push(node);
if (node.itemStyle) {
shapeNormalStyle.push(node.itemStyle.normal);
shapeEmphasisStyle.push(node.itemStyle.emphasis);
}
if (typeof(node.category) !== 'undefined') {
var category = categories[node.category];
if (category) {
// 使用 Legend.getColor 配置默认 category 的默认颜色
category.itemStyle = category.itemStyle || {};
category.itemStyle.normal = category.itemStyle.normal || {};
category.itemStyle.normal.color = category.itemStyle.normal.color
|| legend.getColor(category.name);
queryTarget.push(category);
shapeNormalStyle.unshift(category.itemStyle.normal);
shapeEmphasisStyle.unshift(category.itemStyle.emphasis);
}
}
queryTarget.push(serie);
shapeNormalStyle.unshift(serie.itemStyle.normal.nodeStyle);
shapeEmphasisStyle.unshift(serie.itemStyle.emphasis.nodeStyle);
shape.style.iconType = this.deepQuery(queryTarget, 'symbol');
// 强制设定节点大小,否则默认映射到 minRadius 到 maxRadius 后的值
shape.style.width = shape.style.height
= (this.deepQuery(queryTarget, 'symbolSize') || 0) * 2;
// 节点样式
for (var k = 0; k < shapeNormalStyle.length; k++) {
if (shapeNormalStyle[k]) {
zrUtil.merge(shape.style, shapeNormalStyle[k], true);
}
}
// 节点高亮样式
for (var k = 0; k < shapeEmphasisStyle.length; k++) {
if (shapeEmphasisStyle[k]) {
zrUtil.merge(shape.highlightStyle, shapeEmphasisStyle[k], true);
}
}
// 节点标签样式
if (this.deepQuery(queryTarget, 'itemStyle.normal.label.show')) {
shape.style.text = node.name;
shape.style.textPosition = 'inside';
var labelStyle = this.deepQuery(
queryTarget, 'itemStyle.normal.label.textStyle'
) || {};
shape.style.textColor = labelStyle.color || '#fff';
shape.style.textAlign = labelStyle.align || 'center';
shape.style.textBaseline = labelStyle.baseline || 'middle';
shape.style.textFont = this.getFont(labelStyle);
}
if (this.deepQuery(queryTarget, 'itemStyle.emphasis.label.show')) {
shape.highlightStyle.text = node.name;
shape.highlightStyle.textPosition = 'inside';
var labelStyle = this.deepQuery(
queryTarget, 'itemStyle.emphasis.label.textStyle'
) || {};
shape.highlightStyle.textColor = labelStyle.color || '#fff';
shape.highlightStyle.textAlign = labelStyle.align || 'center';
shape.highlightStyle.textBaseline = labelStyle.baseline || 'middle';
shape.highlightStyle.textFont = this.getFont(labelStyle);
}
// 拖拽特性
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,
// category
{
name : categoryName
},
// series index
0,
// data
node,
// data index
zrUtil.indexOf(this._rawNodes, node),
// name
node.name || '',
// value
node.value
);
this._nodeShapes.push(shape);
this.shapeList.push(shape);
this.zr.addShape(shape);
}
},
_buildLinkShapes: function(serie) {
var nodes = this._filteredNodes;
var links = this._filteredLinks;
var len = links.length;
for (var i = 0; i < len; i++) {
var link = links[i];
var source = nodes[link.source];
var target = nodes[link.target];
var linkShape = new LineShape({
style : {
xStart : 0,
yStart : 0,
xEnd : 0,
yEnd : 0,
lineWidth : 1
},
clickable : true,
highlightStyle : {}
});
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
);
}
}
var link = this._rawLinks[link.rawIndex];
ecData.pack(
linkShape,
// serie
serie,
// serie index
0,
// link data
{
source : link.source,
target : link.target,
weight : link.weight || 0
},
// link data index
link.rawIndex,
// source name - target name
source.name + ' - ' + target.name,
// link weight
link.weight || 0,
// special
// 这一项只是为了表明这是条边
true
);
this._linkShapes.push(linkShape);
this.shapeList.push(linkShape);
this.zr.addShape(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,
opacity: linkShape.style.opacity,
shadowBlur: linkShape.style.shadowBlur,
shadowColor: linkShape.style.shadowColor,
shadowOffsetX: linkShape.style.shadowOffsetX,
shadowOffsetY: linkShape.style.shadowOffsetY
},
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 links = this._filteredLinks;
for (var i = 0, len = links.length; i < len; i++) {
var link = links[i];
var linkShape = this._linkShapes[i];
var sourceShape = this._nodeShapes[link.source];
var targetShape = this._nodeShapes[link.target];
linkShape.style.xStart = sourceShape.position[0];
linkShape.style.yStart = sourceShape.position[1];
linkShape.style.xEnd = targetShape.position[0];
linkShape.style.yEnd = targetShape.position[1];
this.zr.modShape(linkShape.id);
if (linkShape._symbolShape) {
var symbolShape = linkShape._symbolShape;
vec2.copy(symbolShape.position, targetShape.position);
vec2.sub(v, sourceShape.position, targetShape.position);
vec2.normalize(v, v);
vec2.scaleAndAdd(
symbolShape.position, symbolShape.position,
v, targetShape.style.width / 2 + 2
);
var angle;
if (v[1] < 0) {
angle = 2 * Math.PI - Math.acos(-v[0]);
} else {
angle = Math.acos(-v[0]);
}
symbolShape.rotation = angle - Math.PI / 2;
this.zr.modShape(symbolShape.id);
}
}
},
_update: function(e) {
this._layout.temperature = this._temperature;
this._layout.update();
for (var i = 0; i < this._layout.nodes.length; i++) {
var position = this._layout.nodes[i].position;
var shape = this._nodeShapes[i];
var node = this._filteredNodes[i];
if (shape.fixed || (node.fixX && node.fixY)) {
vec2.copy(position, shape.position);
} else if (node.fixX) {
position[0] = shape.position[0];
shape.position[1] = position[1];
} else if (node.fixY) {
position[1] = shape.position[1];
shape.position[0] = position[0];
} else {
vec2.copy(shape.position, position);
}
var nodeName = node.name;
if (nodeName) {
var gPos = this.__nodePositionMap[nodeName];
if (!gPos) {
gPos = this.__nodePositionMap[nodeName] = vec2.create();
}
vec2.copy(gPos, position);
}
}
this._temperature *= this._coolDown;
},
_updateWorker: function(e) {
if (!this._updating) {
return;
}
var positionArr = new Float32Array(e.data);
var token = positionArr[0];
var ret = token === this._token;
// If token is from current layout instance
if (ret) {
var nNodes = (positionArr.length - 1) / 2;
for (var i = 0; i < nNodes; i++) {
var shape = this._nodeShapes[i];
var node = this._filteredNodes[i];
var x = positionArr[i * 2 + 1];
var y = positionArr[i * 2 + 2];
if (shape.fixed || (node.fixX && node.fixY)) {
positionArr[i * 2 + 1] = shape.position[0];
positionArr[i * 2 + 2] = shape.position[1];
} else if (node.fixX) {
positionArr[i * 2 + 1] = shape.position[0];
shape.position[1] = y;
} else if (node.fixY) {
positionArr[i * 2 + 2] = shape.position[1];
shape.position[0] = x;
} else {
shape.position[0] = x;
shape.position[1] = y;
}
var nodeName = node.name;
if (nodeName) {
var gPos = this.__nodePositionMap[nodeName];
if (!gPos) {
gPos = this.__nodePositionMap[nodeName] = vec2.create();
}
vec2.copy(gPos, shape.position);
}
}
this._layoutWorker.postMessage(positionArr.buffer, [positionArr.buffer]);
}
var self = this;
self._layoutWorker.postMessage({
cmd: 'update',
steps: this._steps,
temperature: this._temperature,
coolDown: this._coolDown
});
for (var i = 0; i < this._steps; i++) {
this._temperature *= this._coolDown;
}
return ret;
},
_step: function(e){
if (this._layoutWorker) {
var res = this._updateWorker(e);
if (!res) {
return;
}
} else {
if (this._temperature < 0.01) {
return;
}
this._update();
}
this._updateLinkShapes();
for (var i = 0; i < this._nodeShapes.length; i++) {
this.zr.modShape(this._nodeShapes[i].id);
}
this.zr.refresh();
},
refresh: function(newOption) {
if (newOption) {
this.option = newOption;
this.series = this.option.series;
}
this.clear();
this._buildShape();
},
dispose: function(){
this._updating = false;
this.clear();
this.shapeList = null;
this.effectList = null;
if (this._layoutWorker) {
this._layoutWorker.terminate();
}
this._layoutWorker = null;
this.__nodePositionMap = {};
}
};
/**
* 拖拽开始
*/
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._temperature = 0.8;
}
/**
* 数据项被拖拽出去,重载基类方法
*/
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) {
return [
(Math.random() - 0.5) * size + x,
(Math.random() - 0.5) * size + y
];
}
function _filter(array, callback){
var len = array.length;
var result = [];
for(var i = 0; i < len; i++){
if(callback(array[i], i)){
result.push(array[i]);
}
}
return result;
}
zrUtil.inherits(Force, ChartBase);
zrUtil.inherits(Force, ComponentBase);
// 图表注册
require('../chart').define('force', Force);
return Force;
});