dc.graph
Version:
Graph visualizations integrated with crossfilter and dc.js
277 lines (248 loc) • 9.43 kB
JavaScript
/**
* `dc_graph.d3_force_layout` is an adaptor for d3-force layouts in dc.graph.js
* @class d3_force_layout
* @memberof dc_graph
* @param {String} [id=uuid()] - Unique identifier
* @return {dc_graph.d3_force_layout}
**/
dc_graph.d3_force_layout = function(id) {
var _layoutId = id || uuid();
var _simulation = null; // d3-force simulation
var _dispatch = d3.dispatch('tick', 'start', 'end');
// node and edge objects shared with d3-force, preserved from one iteration
// to the next (as long as the object is still in the layout)
var _nodes = {}, _edges = {};
var _wnodes = [], _wedges = [];
var _options = null;
var _paths = null;
function init(options) {
_options = options;
_simulation = d3.layout.force()
.size([options.width, options.height]);
if(options.linkDistance) {
if(typeof options.linkDistance === 'number')
_simulation.linkDistance(options.linkDistance);
else if(options.linkDistance === 'auto')
_simulation.linkDistance(function(e) {
return e.dcg_edgeLength;
});
}
_simulation.on('tick', /* _tick = */ function() {
dispatchState('tick');
}).on('start', function() {
_dispatch.start();
}).on('end', /* _done = */ function() {
dispatchState('end');
});
}
function dispatchState(event) {
_dispatch[event](
_wnodes,
_wedges.map(function(e) {
return {dcg_edgeKey: e.dcg_edgeKey};
})
);
}
function data(nodes, edges, constraints) {
var nodeIDs = {};
nodes.forEach(function(d, i) {
nodeIDs[d.dcg_nodeKey] = i;
});
_wnodes = regenerate_objects(_nodes, nodes, null, function(v) {
return v.dcg_nodeKey;
}, function(v1, v) {
v1.dcg_nodeKey = v.dcg_nodeKey;
v1.width = v.width;
v1.height = v.height;
v1.id = v.dcg_nodeKey;
if(v.dcg_nodeFixed) {
v1.fixed = true;
v1.x = v.dcg_nodeFixed.x;
v1.y = v.dcg_nodeFixed.y;
} else v1.fixed = false;
});
_wedges = regenerate_objects(_edges, edges, null, function(e) {
return e.dcg_edgeKey;
}, function(e1, e) {
e1.dcg_edgeKey = e.dcg_edgeKey;
// cola edges can work with indices or with object references
// but it will replace indices with object references
e1.source = _nodes[e.dcg_edgeSource];
e1.source.id = nodeIDs[e1.source.dcg_nodeKey];
e1.target = _nodes[e.dcg_edgeTarget];
e1.target.id = nodeIDs[e1.target.dcg_nodeKey];
e1.dcg_edgeLength = e.dcg_edgeLength;
});
_simulation.nodes(_wnodes);
_simulation.links(_wedges);
}
function start() {
installForces();
runSimulation(_options.iterations);
}
function stop() {
if(_simulation)
_simulation.stop();
}
function savePositions() {
var data = {};
Object.keys(_nodes).forEach(function(key) {
data[key] = {x: _nodes[key].x, y: _nodes[key].y};
});
return data;
}
function restorePositions(data) {
Object.keys(data).forEach(function(key) {
if(_nodes[key]) {
_nodes[key].fixed = false;
_nodes[key].x = data[key].x;
_nodes[key].y = data[key].y;
}
});
}
function installForces() {
if(_paths === null)
_simulation.gravity(_options.gravityStrength)
.charge(_options.initialCharge);
else {
if(_options.fixOffPathNodes) {
var nodesOnPath = d3.set(); // nodes on path
_paths.forEach(function(path) {
path.forEach(function(nid) {
nodesOnPath.add(nid);
});
});
// fix nodes not on paths
Object.keys(_nodes).forEach(function(key) {
if(!nodesOnPath.has(key)) {
_nodes[key].fixed = true;
} else {
_nodes[key].fixed = false;
}
});
}
// enlarge charge force to separate nodes on paths
_simulation.charge(_options.chargeForce);
}
};
function runSimulation(iterations) {
if(!iterations) {
dispatchState('end');
return;
}
_simulation.start();
for (var i = 0; i < 300; ++i) {
_simulation.tick();
if(_paths)
applyPathAngleForces();
}
_simulation.stop();
}
function applyPathAngleForces() {
function _dot(v1, v2) { return v1.x*v2.x + v1.y*v2.y; };
function _len(v) { return Math.sqrt(v.x*v.x + v.y*v.y); };
function _angle(v1, v2) {
var a = _dot(v1, v2) / (_len(v1)*_len(v2));
a = Math.min(a, 1);
a = Math.max(a, -1);
return Math.acos(a);
};
// perpendicular unit length vector
function _pVec(v) {
var xx = -v.y/v.x, yy = 1;
var length = _len({x: xx, y: yy});
return {x: xx/length, y: yy/length};
};
function updateNode(node, angle, pVec, alpha) {
node.x += pVec.x*(Math.PI-angle)*alpha;
node.y += pVec.y*(Math.PI-angle)*alpha;
}
_paths.forEach(function(path) {
if(path.length < 3) return; // at least 3 nodes (and 2 edges): A->B->C
for(var i = 1; i < path.length-1; ++i) {
var current = _nodes[path[i]];
var prev = _nodes[path[i-1]];
var next = _nodes[path[i+1]];
// calculate the angle
var vPrev = {x: prev.x - current.x, y: prev.y - current.y};
var vNext = {x: next.x - current.x, y: next.y - current.y};
var angle = _angle(vPrev, vNext); // angle in [0, PI]
var pvecPrev = _pVec(vPrev);
var pvecNext = _pVec(vNext);
// make sure the perpendicular vector is in the
// direction that makes the angle more towards 180 degree
// 1. calculate the middle point of node 'prev' and 'next'
var mid = {x: (prev.x+next.x)/2.0, y: (prev.y+next.y)/2.0};
// 2. calculate the vectors: 'prev' pointing to 'mid', 'next' pointing to 'mid'
var prev_mid = {x: mid.x-prev.x, y: mid.y-prev.y};
var next_mid = {x: mid.x-next.x, y: mid.y-next.y};
// 3. the 'correct' vector: the angle between pvec and prev_mid(next_mid) should
// be an obtuse angle
pvecPrev = _angle(prev_mid, pvecPrev) >= Math.PI/2.0 ? pvecPrev : {x: -pvecPrev.x, y: -pvecPrev.y};
pvecNext = _angle(next_mid, pvecNext) >= Math.PI/2.0 ? pvecNext : {x: -pvecNext.x, y: -pvecNext.y};
// modify positions of prev and next
updateNode(prev, angle, pvecPrev, _options.angleForce);
updateNode(next, angle, pvecNext, _options.angleForce);
}
});
}
var graphviz = dc_graph.graphviz_attrs(), graphviz_keys = Object.keys(graphviz);
var engine = Object.assign(graphviz, {
layoutAlgorithm: function() {
return 'd3-force';
},
layoutId: function() {
return _layoutId;
},
supportsWebworker: function() {
return true;
},
supportsMoving: function() {
return true;
},
parent: property(null),
on: function(event, f) {
if(arguments.length === 1)
return _dispatch.on(event);
_dispatch.on(event, f);
return this;
},
init: function(options) {
this.optionNames().forEach(function(option) {
options[option] = options[option] || this[option]();
}.bind(this));
init(options);
return this;
},
data: function(graph, nodes, edges, constraints) {
data(nodes, edges, constraints);
},
start: function() {
start();
},
stop: function() {
stop();
},
paths: function(paths) {
_paths = paths;
},
savePositions: savePositions,
restorePositions: restorePositions,
optionNames: function() {
return ['iterations', 'angleForce', 'chargeForce', 'gravityStrength',
'initialCharge', 'linkDistance', 'fixOffPathNodes']
.concat(graphviz_keys);
},
iterations: property(300),
angleForce: property(0.02),
chargeForce: property(-500),
gravityStrength: property(1.0),
initialCharge: property(-400),
linkDistance: property(20),
fixOffPathNodes: property(false),
populateLayoutNode: function() {},
populateLayoutEdge: function() {}
});
return engine;
};
dc_graph.d3_force_layout.scripts = ['d3.js'];