mirror of
https://github.com/TBEDP/datavjs.git
synced 2025-12-08 19:45:52 +00:00
425 lines
16 KiB
JavaScript
425 lines
16 KiB
JavaScript
/*global EventProxy, d3, Raphael, self, packages, $ */
|
||
|
||
/*!
|
||
* Bundle的兼容性定义
|
||
*/
|
||
;(function (name, definition) {
|
||
if (typeof define === 'function') { // Module
|
||
define(definition);
|
||
} else { // Assign to common namespaces or simply the global object (window)
|
||
this[name] = definition(function (id) { return this[id];});
|
||
}
|
||
})('Bundle', function (require) {
|
||
var DataV = require('DataV');
|
||
|
||
/**
|
||
* 构造函数,node参数表示在html的哪个容器中绘制该组件
|
||
* options对象为用户自定义的组件的属性,比如画布大小
|
||
*/
|
||
var Bundle = DataV.extend(DataV.Chart, {
|
||
initialize: function (node, options) {
|
||
this.type = "Bundle";
|
||
this.node = this.checkContainer(node);
|
||
this.json = {};
|
||
|
||
// 图的半径
|
||
this.defaults.diameter = 960;
|
||
this.defaults.radius = this.defaults.diameter / 2;
|
||
this.defaults.innerRadius = this.defaults.radius - 120;
|
||
this.defaults.tension = 0.85;
|
||
|
||
this.defaults.color = {
|
||
defaultLineColor: "#4065AF",
|
||
defaultWordColor: "#000000",
|
||
lineHoverColor: "#02B0ED",
|
||
nodeHoverColor: "#02B0ED",
|
||
importNodesColor: "#5DA714", //被引用的节点
|
||
exportNodesColor: "#FE3919" //引用当前节点的节点
|
||
};
|
||
|
||
this.setOptions(options);
|
||
this.createCanvas();
|
||
}
|
||
});
|
||
|
||
/**
|
||
* 设置用户自定义属性
|
||
*/
|
||
Bundle.prototype.setOptions = function (options) {
|
||
if (options) {
|
||
var prop;
|
||
for (prop in options) {
|
||
if (options.hasOwnProperty(prop)) {
|
||
this.defaults[prop] = options[prop];
|
||
if (prop === "diameter") {
|
||
this.defaults.radius = this.defaults.diameter / 2;
|
||
this.defaults.innerRadius = this.defaults.radius - 120;
|
||
} else if (prop === "radius") {
|
||
this.defaults.diameter = this.defaults.radius * 2;
|
||
this.defaults.innerRadius = this.defaults.radius - 120;
|
||
} else if (prop === "innerRadius") {
|
||
this.defaults.radius = this.defaults.innerRadius + 120;
|
||
this.defaults.diameter = this.defaults.radius * 2;
|
||
} else if (prop === "width") {
|
||
this.defaults.diameter = this.defaults.width;
|
||
this.defaults.radius = this.defaults.diameter / 2;
|
||
this.defaults.innerRadius = this.defaults.radius - 120;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 对原始数据进行处理
|
||
* TODO: 改进为获取值时运算
|
||
*/
|
||
Bundle.prototype.setSource = function (source) {
|
||
if (source[0] && source[0] instanceof Array) {
|
||
// csv or 2d array source
|
||
if (source[0][0] === "name") {
|
||
source = source.slice(1); // 从第一行开始,第0行舍去
|
||
}
|
||
var nData = [];
|
||
var imports = [];
|
||
//var isNode = true;
|
||
var nodeNum;
|
||
var that = this;
|
||
source.forEach(function (d, i) {
|
||
if (d[0] === "") {
|
||
throw new Error("name can not be empty(line:" + (i + 1) + ").");
|
||
}
|
||
if (d[1] !== "") {
|
||
imports = d[1].split(" ");
|
||
}
|
||
nData[i] = {
|
||
name: d[0],
|
||
imports: imports
|
||
};
|
||
});
|
||
this.json = nData;
|
||
} else {
|
||
// json source
|
||
this.json = source;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 创建画布
|
||
*/
|
||
Bundle.prototype.createCanvas = function () {
|
||
var conf = this.defaults;
|
||
this.canvas = new Raphael(this.node, conf.diameter, conf.diameter);
|
||
|
||
//var c = this.canvas.circle(50, 50, 40);
|
||
};
|
||
|
||
/**
|
||
* 布局
|
||
*/
|
||
Bundle.prototype.layout = function () {
|
||
var packages = {
|
||
// Lazily construct the package hierarchy from class names.
|
||
root: function (classes) {
|
||
var map = {};
|
||
function construct(name, data) {
|
||
var node = map[name], i;
|
||
if (!node) {
|
||
node = map[name] = data || {name: name, children: []};
|
||
if (name.length) {
|
||
node.parent = construct(name.substring(0, i = name.lastIndexOf(".")));
|
||
node.parent.children.push(node);
|
||
node.key = name.substring(i + 1);
|
||
}
|
||
}
|
||
return node;
|
||
}
|
||
|
||
classes.forEach(function (d) {
|
||
construct(d.name, d);
|
||
});
|
||
|
||
return map[""];
|
||
},
|
||
|
||
// Return a list of imports for the given array of nodes.
|
||
imports: function (nodes) {
|
||
var map = {},
|
||
imports = [];
|
||
|
||
// Compute a map from name to node.
|
||
nodes.forEach(function (d) {
|
||
map[d.name] = d;
|
||
});
|
||
|
||
// For each import, construct a link from the source to target node.
|
||
nodes.forEach(function (d) {
|
||
if (d.imports) {
|
||
d.imports.forEach(function (i) {imports.push({source: map[d.name], target: map[i]});
|
||
});
|
||
}
|
||
});
|
||
|
||
return imports;
|
||
}
|
||
};
|
||
|
||
var cluster = d3.layout.cluster()
|
||
.size([360, this.defaults.innerRadius]) //.size(角度,半径)
|
||
.sort(null)
|
||
.value(function (d) {
|
||
return d.size;
|
||
});
|
||
this.nodes = cluster.nodes(packages.root(this.json));
|
||
this.links = packages.imports(this.nodes);
|
||
};
|
||
|
||
/**
|
||
* 渲染弦图
|
||
*/
|
||
Bundle.prototype.render = function () {
|
||
this.layout();
|
||
this.generatePaths();
|
||
};
|
||
|
||
/**
|
||
* 生成路径
|
||
*/
|
||
Bundle.prototype.generatePaths = function (options) {
|
||
var that = this;
|
||
|
||
var canvas = this.canvas;
|
||
var rNodes = canvas.set();
|
||
var rLinks = canvas.set();
|
||
|
||
var bundle = d3.layout.bundle();
|
||
|
||
var line = d3.svg.line.radial()
|
||
.interpolate("bundle")
|
||
.tension(this.defaults.tension)
|
||
.radius(function (d) {
|
||
return d.y;
|
||
})
|
||
.angle(function (d) {
|
||
return d.x / 180 * Math.PI;
|
||
});
|
||
|
||
//定义图中的弦和节点
|
||
var nodes = this.nodes;
|
||
var links = this.links;
|
||
var linksCount = links.length;
|
||
var paths = bundle(links);
|
||
|
||
var locateStr = ""; //对文字进行平移和旋转
|
||
var locateBBox = ""; //对文字的bounding box进行平移和旋转
|
||
var r = 0;
|
||
var angle = 0;
|
||
var xTrans = 0;
|
||
var yTrans = 0;
|
||
var anchor; //text-anchor: start or end
|
||
var rotateStr = "";
|
||
|
||
//element data cache
|
||
var nodeRelatedElements = {};// {key: {targetLink: [], sourceLink: [], targetNode: [], sourceNode: []}}
|
||
var nodeElements = {}; //{key: Els}
|
||
var bBoxElements = {}; //{key: Els}
|
||
|
||
var i,
|
||
j,
|
||
key,
|
||
textEl,
|
||
bBox,
|
||
bBoxNew,
|
||
tCenterX,
|
||
tCenterY,
|
||
bBoxEl,
|
||
linkEl;
|
||
|
||
var mouseoverLink = function () {
|
||
var current = this;
|
||
//var color = that.data("color");
|
||
if (rLinks.preLink) {
|
||
rLinks.preLink.attr("stroke", that.defaults.color.defaultLineColor)
|
||
.attr("stroke-width", 1)
|
||
.attr("stroke-opacity", 0.6);
|
||
|
||
}
|
||
rLinks.preLink = this;
|
||
|
||
current.attr("stroke", that.defaults.color.lineHoverColor)
|
||
.attr("stroke-width", 2)
|
||
.attr("stroke-opacity", 1.0)
|
||
.toFront(); //把当前弦移到画布最上层
|
||
};
|
||
|
||
var mouseoverNode = function () {
|
||
var relatedEl = this.data("relatedElements");
|
||
//高亮所选节点的文字颜色
|
||
this.data("relatedNode").attr({"fill": that.defaults.color.nodeHoverColor,
|
||
"fill-opacity": 1.0, "font-weight": "600"});
|
||
//将包围盒颜色设为透明
|
||
this.attr({"fill": that.defaults.color.nodeHoverColor, "fill-opacity": 0.0/*, "font-weight": "600"*/});
|
||
|
||
relatedEl.sourceLink.forEach(function (d) { //set green
|
||
d.attr({"stroke": that.defaults.color.importNodesColor, "stroke-width": 1, "stroke-opacity": 0.9})
|
||
.toFront();
|
||
});
|
||
relatedEl.sourceNode.forEach(function (d) {
|
||
d.attr({"fill": that.defaults.color.importNodesColor, "font-weight": "600"});
|
||
});
|
||
relatedEl.targetLink.forEach(function (d) { //set red
|
||
d.attr({"stroke": that.defaults.color.exportNodesColor, "stroke-width": 1, "stroke-opacity": 0.9})
|
||
.toFront();
|
||
});
|
||
relatedEl.targetNode.forEach(function (d) {
|
||
d.attr({"fill": that.defaults.color.exportNodesColor, "font-weight": "600"});
|
||
});
|
||
};
|
||
|
||
var mouseoutNode = function () {
|
||
var relatedEl = this.data("relatedElements");
|
||
this.data("relatedNode").attr({"fill": that.defaults.color.defaultWordColor,
|
||
"font-weight": "400", "fill-opacity": 1.0});
|
||
relatedEl.targetLink.forEach(function (d) {
|
||
d.attr({"stroke": that.defaults.color.defaultLineColor, "stroke-width": 1, "stroke-opacity": 0.6});
|
||
});
|
||
relatedEl.targetNode.forEach(function (d) {
|
||
d.attr({"fill": that.defaults.color.defaultWordColor, "font-weight": "400"});
|
||
});
|
||
relatedEl.sourceLink.forEach(function (d) {
|
||
d.attr({"stroke": that.defaults.color.defaultLineColor, "stroke-width": 1, "stroke-opacity": 0.6});
|
||
});
|
||
relatedEl.sourceNode.forEach(function (d) {
|
||
d.attr({"fill": that.defaults.color.defaultWordColor, "font-weight": "400"});
|
||
});
|
||
};
|
||
|
||
for (j = 0; j < nodes.length; j++) {
|
||
//若为叶子节点
|
||
if (!nodes[j].children) {
|
||
locateStr = "T" + that.defaults.radius + "," + that.defaults.radius + "R"; //使用大写T、R、S--绝对,not相对
|
||
|
||
//半径: add a padding between lines and words
|
||
r = nodes[j].y + 20;
|
||
|
||
//计算旋转角度和水平、竖直方向所需平移的距离
|
||
angle = (nodes[j].x - 90) * Math.PI / 180;
|
||
xTrans = r * Math.cos(angle);
|
||
yTrans = r * Math.sin(angle);
|
||
|
||
//计算text-anchor
|
||
if (nodes[j].x < 180) {
|
||
anchor = "start";
|
||
} else {
|
||
anchor = "end";
|
||
}
|
||
|
||
//计算文字方向是否需要旋转180度
|
||
if (nodes[j].x < 180) {
|
||
rotateStr = "";
|
||
} else {
|
||
rotateStr = "R180";
|
||
}
|
||
|
||
//计算文字需要如何经过平移和旋转被排列在圆周上
|
||
locateStr += (nodes[j].x - 90) + rotateStr + "T" + xTrans + "," + yTrans;
|
||
|
||
//绘制文字
|
||
textEl = canvas.text()
|
||
.attr("font", "11px arial")
|
||
.data("color", that.defaults.color)
|
||
.attr("text", nodes[j].key)
|
||
//.attr("title", nodes[j].size)
|
||
.transform(locateStr)
|
||
.attr("text-anchor", anchor)
|
||
.attr("fill", that.defaults.color.defaultWordColor);
|
||
|
||
//获取旋转平移之前文字的bounding box
|
||
bBox = textEl.getBBox(true);
|
||
|
||
//canvas.rect(bBox.x, bBox.y, bBox.width, bBox.height);
|
||
//获取旋转平移之后文字的bounding box
|
||
bBoxNew = textEl.getBBox();
|
||
//adjust vml box center
|
||
if (Raphael.vml) {
|
||
//vml's word bbox is not related to text-anchor, always middle;
|
||
//svg's word bbox is related to text-anchor;
|
||
bBoxNew.x = bBoxNew.x + bBox.width / 2 * Math.cos(angle);
|
||
bBoxNew.y = bBoxNew.y + bBox.width / 2 * Math.sin(angle);
|
||
}
|
||
//canvas.rect(bBoxNew.x, bBoxNew.y, bBoxNew.width, bBoxNew.height);
|
||
|
||
//新旧bounding box的中心坐标变化
|
||
tCenterX = bBoxNew.x + bBoxNew.width / 2 - bBox.x - bBox.width / 2;
|
||
tCenterY = bBoxNew.y + bBoxNew.height / 2 - bBox.y - bBox.height / 2;
|
||
//对bounding box进行平移和旋转
|
||
locateBBox = "T" + tCenterX + "," + tCenterY + "R" + (nodes[j].x - 90) + rotateStr;
|
||
|
||
// 包围盒
|
||
bBoxEl = canvas.rect(bBox.x, bBox.y, bBox.width, bBox.height)
|
||
.transform(locateBBox)
|
||
.data("relatedNode", textEl)
|
||
.attr({"fill": "#fff", "opacity": 0.01});
|
||
|
||
key = nodes[j].key;
|
||
nodeElements[key] = textEl;
|
||
bBoxElements[key] = bBoxEl;
|
||
nodeRelatedElements[key] = {targetLink: [], sourceLink: [], targetNode: [], sourceNode: []};
|
||
|
||
rNodes.push(textEl);
|
||
}
|
||
}
|
||
|
||
//绘制曲线
|
||
for (i = 0; i < linksCount; i++) {
|
||
var l = paths[i];
|
||
|
||
//对paths数组中的每一项进行计算,由路径节点信息得到坐标值
|
||
var spline = line(l);
|
||
var sourceKey = links[i].source.key;
|
||
var targetKey = links[i].target.key;
|
||
var tips = "link source: " + sourceKey + "\n"
|
||
+ "link target: " + targetKey;
|
||
|
||
linkEl = canvas.path(spline)
|
||
//.attr("stroke", that.defaults.defaultLineColor)
|
||
.attr("stroke-opacity", 0.6)
|
||
.attr("title", tips)
|
||
.attr("d", spline)
|
||
.attr("stroke", that.defaults.color.defaultLineColor)
|
||
.translate(that.defaults.radius, that.defaults.radius)
|
||
.mouseover(mouseoverLink);
|
||
//.mouseout(mouseoutLink);
|
||
linkEl[0].el = linkEl;
|
||
|
||
nodeRelatedElements[sourceKey].targetLink.push(linkEl);
|
||
nodeRelatedElements[sourceKey].targetNode.push(nodeElements[targetKey]);
|
||
nodeRelatedElements[targetKey].sourceLink.push(linkEl);
|
||
nodeRelatedElements[targetKey].sourceNode.push(nodeElements[sourceKey]);
|
||
rLinks.push(linkEl);
|
||
}
|
||
|
||
$(this.canvas.canvas).mousemove(function (e) {
|
||
if(!e.target.el && rLinks.preLink){
|
||
rLinks.preLink.attr("stroke", that.defaults.color.defaultLineColor)
|
||
.attr("stroke-width", 1)
|
||
.attr("stroke-opacity", 0.6);
|
||
rLinks.preLink = undefined;
|
||
//console.log("a");
|
||
}
|
||
});
|
||
|
||
//bind text words hover event
|
||
for (key in bBoxElements) {
|
||
if (bBoxElements.hasOwnProperty(key)) {
|
||
bBoxElements[key].data("relatedElements", nodeRelatedElements[key])
|
||
.mouseover(mouseoverNode)
|
||
.mouseout(mouseoutNode);
|
||
}
|
||
}
|
||
|
||
};
|
||
|
||
return Bundle;
|
||
});
|