datavjs/lib/charts/force.js
2012-10-12 11:01:11 +08:00

683 lines
26 KiB
JavaScript

/*global Raphael, d3 */
;(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];
});
}
})('Force', function (require) {
var DataV = require('DataV');
var Force = DataV.extend(DataV.Chart, {
initialize: function (container, options) {
this.type = "Force";
this.container = container;
this.defaults = {};
this.net = {};
this.linkValeMin = 0;
this.linkValeMax = 1;
this.nodeValueMin = 0;
this.nodeValueMax = 1;
this.clicked = false;
this.clickedNum = -1;
this.tagClicked = false;
// Properties
this.font = {};
// Canvas
this.defaults.tag = true;
this.defaults.width = 500;
this.defaults.height = 500;
this.defaults.linkLength = 50;
this.defaults.linkWidth = 2;
this.defaults.classNum = 6;
this.defaults.forceValue = 10;
this.defaults.iterate = 100;
this.defaults.browserName = navigator.appName;
this.setOptions(options);
this.defaults.charge = -(this.defaults.width + this.defaults.height) / this.defaults.forceValue;
this.tagArea = [20, (this.defaults.height - 20 - this.defaults.classNum * 20), 200, 220];
if (this.defaults.tag) {
this.xOffset = this.tagArea[2];
} else {
this.xOffset = 0;
}
this.createCanvas();
}
});
//Strings in CSV to Numbers
Force.prototype._toNum = function (value) {
var type = typeof value;
if (type === "number") {
return value;
} else if (type === "string" && value !== "") {
return parseInt(value, 10);
} else {
return 1;
}
};
//Set CSV content to force-directed net
Force.prototype.setSource = function (table) {
//this.net = json;
if (table[0][0] === "Id") {
table = table.slice(1);
}
var nData = [];
var lData = [];
var isNode = true;
var nodeNum;
var that = this;
table.forEach(function (d, i) {
var value;
if (isNode) {
if (d[0] === "Source") {
isNode = false;
nodeNum = i + 1;
} else {
if (d[0] === "") {
throw new Error("ID can not be empty(line:" + (i + 1) + ").");
}
value = that._toNum(d[2]);
nData[i] = {
name: d[1],
nodeValue: value
};
if (i === 0) {
that.nodeValueMin = value;
that.nodeValueMax = value;
}
that.nodeValueMin = (value < that.nodeValueMin) ? value : that.nodeValueMin;
that.nodeValueMax = (value > that.nodeValueMax) ? value : that.nodeValueMax;
}
} else {
if (d[0] === "") {
throw new Error("Source can not be empty(line:" + (i + 1) + ").");
}
if (d[1] === "") {
throw new Error("Target can not be empty(line:" + (i + 1) + ").");
}
value = that._toNum(d[2]);
lData[i - nodeNum] = {
source: that._toNum(d[0]),
target: that._toNum(d[1]),
value: that._toNum(d[2])
};
if (i === nodeNum) {
that.linkValueMin = value;
that.linkValueMax = value;
}
that.linkValueMin = (value < that.linkValueMin) ? value : that.linkValueMin;
that.linkValueMax = (value > that.linkValueMax) ? value : that.linkValueMax;
}
});
this.net.nodes = nData;
this.net.links = lData;
this.nodeValueMax++;
this.linkValueMax++;
};
Force.prototype.createCanvas = function () {
var conf = this.defaults;
this.canvas = new Raphael(this.container, conf.width, conf.height);
//var c = this.canvas.circle(50, 50, 40);
};
Force.prototype.getColor = function (i) {
var color = DataV.getColor(this.classNum);
//var k = color.length * (i - this.nodeValueMin-0.1) / (this.nodeValueMax - this.nodeValueMin);
//if (k < 0) k = 0;
return color[i % color.length][0];
};
Force.prototype.getRadius = function (value) {
var conf = this.defaults;
return 16.0 * (value - this.nodeValueMin) / (this.nodeValueMax - this.nodeValueMin) + 8;
};
Force.prototype.getOpacity = function (value) {
return 0.083 * (value - this.linkValueMin) / (this.linkValueMax - this.linkValueMin) + 0.078;
};
//update the layout by modify the attributes of nodes and links
Force.prototype.update = function () {
var that = this;
var conf = this.defaults;
var canvas = this.canvas;
this.nodes = this.canvas.set();
this.links = this.canvas.set();
var nodes = this.nodes;
var links = this.links;
var i, j, temp;
this.force.charge(conf.charge).nodes(this.net.nodes).links(this.net.links).start();
var nodesData = this.net.nodes;
var linksData = this.net.links;
var nodesNum = nodesData.length;
var linksNum = linksData.length;
var connectMatrix = [];
var linkMatrix = [];
conf.iterate = (nodesNum + linksNum) * 2;
var onMouseClick = function () {
that.tagClicked = false;
that.underBn.forEach(function (d) {
d.hide();
d.data('clicked', false);
});
that.clicked = true;
if (!this.data('clicked')) {
if (conf.browserName !== "Microsoft Internet Explorer") {
that.force.linkDistance(conf.linkLength * 2).charge(conf.charge * 2).start();
}
that.nodes.forEach(function (d) {
d.data('rect').hide();
d.data('text').hide();
d.attr({
"opacity": 0.2
});
d.data('clicked', false);
d.data('showText', false);
});
that.links.forEach(function (d) {
d.attr({
'stroke-opacity': 0.0
});
});
that.clickedNum = this.data('index');
this.data('clicked', true);
this.data("link").forEach(function (d) {
d.attr({
"stroke-opacity": d.data('opacity')
});
});
this.data("node").forEach(function (d) {
d.attr({
"opacity": 0.9
});
d.data('showText', true);
});
that.underBn[this.data('colorType')].data('clicked', true).attr('opacity', 1).show();
} else {
that.clicked = false;
if (conf.browserName !== "Microsoft Internet Explorer") {
that.force.linkDistance(conf.linkLength).charge(conf.charge).start();
}
nodes.forEach(function (d) {
d.attr({
"opacity": 0.9
});
if (d.data('big')) {
d.data('showText', true);
} else {
d.data('rect').hide();
d.data('text').hide();
d.data('showText', false);
}
});
links.forEach(function (d) {
d.attr({
'stroke-opacity': d.data('opacity')
});
});
this.data('clicked', false);
that.underBn[this.data('colorType')].hide();
}
}
var onCanvasClick = function () {
that.tagClicked = false;
that.underBn.forEach(function (d) {
d.hide();
d.data('clicked', false);
});
that.clicked = false;
if (conf.browserName !== "Microsoft Internet Explorer") {
that.force.linkDistance(conf.linkLength).charge(conf.charge).start();
} else {
that.force.resume();
}
nodes.forEach(function (d) {
d.attr({
"opacity": 0.9
});
if (d.data('big')) {
d.data('showText', true);
} else {
d.data('rect').hide();
d.data('text').hide();
d.data('showText', false);
}
});
links.forEach(function (d) {
d.attr({
'stroke-opacity': d.data('opacity')
});
});
};
var topValue = [];
var topId = [];
var topNum = 10;
if (nodesNum < 10) {
topNum = nodesNum;
}
for (i = 0; i < topNum; i++) {
topValue[i] = nodesData[i].nodeValue;
topId[i] = i;
}
for (i = 0; i < topNum; i++) {
for (j = 1; j < topNum - i; j++) {
if (topValue[j] < topValue[j - 1]) {
temp = topValue[j];
topValue[j] = topValue[j - 1];
topValue[j - 1] = temp;
temp = topId[j];
topId[j] = topId[j - 1];
topId[j - 1] = temp;
}
}
}
//rapheal绘制部分
for (i = 0; i < nodesNum; i++) {
nodesData[i].x = (conf.width + this.xOffset) / 2;
nodesData[i].y = conf.height / 2;
var n = nodesData[i];
var k = Math.floor(conf.classNum * (n.nodeValue - this.nodeValueMin) / (this.nodeValueMax - this.nodeValueMin));
if (k >= conf.classNum) k = conf.classNum - 1;
var radius = this.getRadius(n.nodeValue);
var cnode = canvas.circle(n.x, n.y, radius).attr({
fill: this.getColor(k),
'stroke': "#ffffff",
'opacity': 0.9
//title: n.name
});
var nodeText = canvas.text(n.x, n.y - radius, n.name).attr({
'opacity': 1,
//'font-family': "微软雅黑",
'font': '12px Verdana'
}).hide();
var nodeRect = canvas.rect(n.x, n.y, nodeText.getBBox().width, nodeText.getBBox().height, 2).attr({
'fill': "#000000",
'stroke-opacity': 0,
'fill-opacity': 0.1
}).hide();
cnode.data('r', radius);
cnode.data("name", n.name);
cnode.data('text', nodeText);
cnode.data('rect', nodeRect);
cnode.data('colorType', k);
cnode.data('clicked', false);
cnode.data('index', i);
cnode.data('big', false);
if (i >= topNum && topValue[0] < nodesData[i].nodeValue) {
topValue[0] = nodesData[i].nodeValue;
topId[0] = i;
for (j = 1; j < topNum; j++) {
if (topValue[j] < topValue[j - 1]) {
temp = topValue[j];
topValue[j] = topValue[j - 1];
topValue[j - 1] = temp;
temp = topId[j];
topId[j] = topId[j - 1];
topId[j - 1] = temp;
} else {
break;
}
}
}
nodes.push(cnode);
connectMatrix[i] = [];
linkMatrix[i] = [];
connectMatrix[i].push(nodes[i]);
}
for (i = 0; i < topNum; i++) {
nodes[topId[i]].data("big", true);
}
for (i = 0; i < linksNum; i++) {
var l = linksData[i];
var clink = canvas.path("M" + l.source.x + "," + l.source.y + "L" + l.target.x + "," + l.target.y).attr({
'stroke-width': conf.linkWidth,
'stroke-opacity': this.getOpacity(l.value)
}).toBack();
clink.data('opacity', this.getOpacity(l.value));
links.push(clink);
connectMatrix[l.source.index].push(nodes[l.target.index]);
connectMatrix[l.target.index].push(nodes[l.source.index]);
linkMatrix[l.source.index].push(links[i]);
linkMatrix[l.target.index].push(links[i]);
}
var background = canvas.rect(0, 0, conf.width, conf.height).attr({
'fill': '#ffffff',
'stroke-opacity': 0
}).toBack();
background.click(onCanvasClick);
nodes.forEach(function (d, i) {
d.data("node", connectMatrix[i]);
d.data("link", linkMatrix[i]);
if (d.data('big')) {
d.data('showText', true);
} else {
d.data('showText', false);
}
d.drag(function (dx, dy) {
d.data('x', this.ox + dx);
d.data('y', this.oy + dy);
}, function () {
that.force.resume();
this.ox = this.attr("cx");
this.oy = this.attr("cy");
d.data('x', this.ox);
d.data('y', this.oy);
d.data('drag', true);
}, function () {
that.force.resume();
d.data('drag', false);
});
d.click(onMouseClick); //.mouseup(onmouseup);
d.mouseover(function () {
if (conf.browserName !== "Microsoft Internet Explorer") {
that.force.resume();
}
this.attr({
'r': d.data('r') + 5
});
if (!this.data('showText')) {
//this.attr('title', "");
this.data('showText', true);
this.data('hover', true);
}
if (!that.underBn[this.data('colorType')].data('clicked')) {
that.underBn[this.data('colorType')].attr('opacity', 0.5).show();
}
}).mouseout(function () {
this.attr({
'r': d.data('r')
});
//this.attr('title', this.data('name'));
if (this.data('hover') && !this.data('clicked')) {
d.data('rect').hide();
d.data('text').hide();
this.data('showText', false);
this.data('hover', false);
}
if (!that.underBn[this.data('colorType')].data('clicked')) {
that.underBn[this.data('colorType')].hide();
}
});
});
};
Force.prototype.tag = function () {
var that = this;
var conf = this.defaults;
var paper = this.canvas;
var tagArea = this.tagArea;
var rectBn = paper.set();
this.underBn = [];
var underBn = this.underBn;
for (i = 0; i <= conf.classNum - 1; i++) {
//底框
underBn.push(paper.rect(tagArea[0] + 10, tagArea[1] + 10 + (20 + 3) * i, 180, 20).attr({
"fill": "#ebebeb",
"stroke": "none",
'opacity': 1
}).data('clicked', false).hide());
//色框
paper.rect(tagArea[0] + 10 + 3, tagArea[1] + 10 + (20 + 3) * i + 6, 16, 8).attr({
"fill": that.getColor(i),
"stroke": "none"
});
//文字
var min = Math.floor(this.nodeValueMin + i * (this.nodeValueMax - this.nodeValueMin) / conf.classNum);
var max = Math.floor(min + (this.nodeValueMax - this.nodeValueMin) / conf.classNum);
paper.text(tagArea[0] + 10 + 3 + 16 + 8, tagArea[1] + 10 + (20 + 3) * i + 10, min + " ~ " + max).attr({
"fill": "black",
"fill-opacity": 1,
"font-family": "Verdana",
"font-size": 12
}).attr({
"text-anchor": "start"
});
//选框
rectBn.push(paper.rect(tagArea[0] + 10, tagArea[1] + 10 + (20 + 3) * i, 180, 20).attr({
"fill": "white",
"fill-opacity": 0,
"stroke": "none"
//"r": 3
})).data("clicked", 0);
}
rectBn.forEach(function (d, i) {
d.mouseover(function () {
if (!underBn[i].data('clicked')) {
underBn[i].attr('opacity', 0.5);
underBn[i].show();
}
}).mouseout(function () {
if (!underBn[i].data('clicked')) {
underBn[i].hide();
}
});
d.click(function () {
that.clicked = false;
if (conf.browserName !== "Microsoft Internet Explorer") {
that.force.linkDistance(conf.linkLength).charge(conf.charge).start();
}
for (j = 0; j < underBn.length; j++) {
if (j === i) {
underBn[j].show();
} else {
underBn[j].hide();
}
}
rectBn.forEach(function (eachBn) {
if (eachBn !== d) {
eachBn.data("clicked", 0);
}
});
if (d.data("clicked") === 0) {
that.tagClicked = true;
underBn[i].attr('opacity', 1);
underBn[i].data('clicked', true);
underBn[i].show();
that.nodes.forEach(function (d) {
if (d.data('colorType') === i) {
d.attr({
"opacity": 0.9
});
d.data('showText', true);
} else {
d.attr({
"opacity": 0.2
});
d.data('rect').hide();
d.data('text').hide();
d.data('showText', false);
}
});
that.links.forEach(function (d) {
d.attr({
"stroke-opacity": 0
});
});
d.data("clicked", 1);
} else if (d.data("clicked") === 1) {
that.tagClicked = false;
underBn[i].data('clicked', false);
underBn[i].hide();
d.data("clicked", 0);
that.nodes.forEach(function (d) {
d.attr({
"opacity": 0.9
});
if (d.data('big')) {
d.data('showText', true);
} else {
d.data('rect').hide();
d.data('text').hide();
d.data('showText', false);
}
});
that.links.forEach(function (d) {
d.attr({
"stroke-opacity": d.data('opacity')
});
});
}
});
});
}
Force.prototype.layout = function () {
var conf = this.defaults;
this.force = d3.layout.force().linkDistance(conf.linkLength).size([conf.width + this.xOffset, conf.height]).theta(1.5);
};
Force.prototype.animate = function () {
var conf = this.defaults;
var nodes = this.nodes;
var links = this.links;
var tick = 0;
var that = this;
var nodesData = this.net.nodes;
var linksData = this.net.links;
this.force.on("tick", function () {
if (conf.browserName !== "Microsoft Internet Explorer" || tick > conf.iterate) {
if (tick % 2 === 0) {
nodes.forEach(function (d, i) {
var margin = d.data('r');
var nd = nodesData[i];
if (d.data('drag')) {
nd.x = d.data('x');
nd.y = d.data('y');
}
nd.x = (nd.x < margin + that.xOffset) ? (margin + that.xOffset) : nd.x;
nd.x = (nd.x > conf.width - margin) ? conf.width - margin : nd.x;
nd.y = (nd.y < margin) ? margin : nd.y;
nd.y = (nd.y > conf.height - margin) ? conf.height - margin : nd.y;
var bx = d.data('text').getBBox().width / 2;
var by = d.data('text').getBBox().height / 2;
if (that.clicked) {
var mx = nodesData[that.clickedNum].x;
var my = nodesData[that.clickedNum].y;
var tx, ty;
if (d.data('clicked')) {
if (conf.browserName !== "Microsoft Internet Explorer") {
nd.x = (conf.width + that.xOffset) / 2;
nd.y = conf.height / 2;
}
d.data('rect').attr({
'x': nd.x - bx,
'y': nd.y + d.data('r')
});
d.data('text').attr({
'x': nd.x,
'y': nd.y + by + d.data('r')
});
d.data('rect').show();
d.data('text').show();
} else if (d.data('showText')) {
var lx = (nd.x - mx);
var ly = (nd.y - my);
var length = Math.sqrt(lx * lx + ly * ly);
tx = nd.x + bx * lx / length;
ty = nd.y + by * ly / length;
tx = (nd.x < mx) ? tx - d.data('r') : tx + d.data('r');
ty = (nd.y < my) ? ty - d.data('r') : ty + d.data('r');
tx = (tx < margin + that.xOffset) ? (margin + that.xOffset) : tx;
tx = (tx > conf.width - margin) ? conf.width - margin : tx;
ty = (ty < margin) ? margin : ty;
ty = (ty > conf.height - margin) ? conf.height - margin : ty;
d.data('rect').attr({
'x': tx - bx,
'y': ty - by
});
d.data('text').attr({
'x': tx,
'y': ty
});
d.data('rect').show();
d.data('text').show();
}
} else if (d.data('showText')) {
d.data('rect').attr({
'x': nd.x - bx,
'y': nd.y - by + 4 + d.data('r')
});
d.data('text').attr({
'x': nd.x,
'y': nd.y + 4 + d.data('r')
});
try {
d.data('rect').show();
d.data('text').show();
} catch (e) {}
}
d.attr({
'cx': nd.x,
'cy': nd.y
});
});
links.forEach(function (d, i) {
d.attr('path', "M" + linksData[i].source.x + "," + linksData[i].source.y + "L" + linksData[i].target.x + "," + linksData[i].target.y);
});
}
}++tick;
});
};
Force.prototype.setOptions = function (options) {
var prop;
if (options) {
for (prop in options) {
if (options.hasOwnProperty(prop)) {
this.defaults[prop] = options[prop];
}
}
}
};
//render the force-directed net on the canvas and keep updating
Force.prototype.render = function (options) {
if (!this.container) {
throw new Error("Please specify which node to render.");
}
this.setOptions(options);
this.canvas.clear();
this.layout();
if (this.defaults.tag) {
this.tag();
}
this.update();
var i;
this.animate();
};
return Force;
});