/**
* `dc_graph.d3v4_force_layout` is an adaptor for d3-force version 4 layouts in dc.graph.js
* @class d3v4_force_layout
* @memberof dc_graph
* @param {String} [id=uuid()] - Unique identifier
* @return {dc_graph.d3v4_force_layout}
**/
dc_graph.d3v4_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 = d3v4.forceSimulation()
.force('link', d3v4.forceLink())
.force('center', d3v4.forceCenter(options.width / 2, options.height / 2))
.force('gravityX', d3v4.forceX(options.width / 2).strength(_options.gravityStrength))
.force('gravityY', d3v4.forceY(options.height / 2).strength(_options.gravityStrength))
.force('collision', d3v4.forceCollide(_options.collisionRadius))
.force('charge', d3v4.forceManyBody())
.stop();
}
function dispatchState(event) {
_dispatch[event](
_wnodes,
_wedges.map(function(e) {
return {dcg_edgeKey: e.dcg_edgeKey};
})
);
}
function data(nodes, edges) {
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.fx = v.dcg_nodeFixed.x;
v1.fy = v.dcg_nodeFixed.y;
} else v1.fx = v1.fy = null;
});
_wedges = regenerate_objects(_edges, edges, null, function(e) {
return e.dcg_edgeKey;
}, function(e1, e) {
e1.dcg_edgeKey = e.dcg_edgeKey;
e1.source = nodeIDs[_nodes[e.dcg_edgeSource].dcg_nodeKey];
e1.target = nodeIDs[_nodes[e.dcg_edgeTarget].dcg_nodeKey];
e1.dcg_edgeLength = e.dcg_edgeLength;
});
_simulation.force('straighten', null);
_simulation.nodes(_wnodes);
_simulation.force('link').links(_wedges);
}
function start() {
_dispatch.start();
installForces(_paths);
runSimulation(_options.iterations);
}
function stop() {
// not running asynchronously, no _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].fx = data[key].x;
_nodes[key].fy = data[key].y;
}
});
}
function installForces(paths) {
if(paths)
paths = paths.filter(function(path) {
return path.nodes.every(function(nk) { return _nodes[nk]; });
});
if(paths === null || !paths.length) {
_simulation.force('charge').strength(_options.initialCharge);
} else {
var nodesOnPath;
if(_options.fixOffPathNodes) {
nodesOnPath = d3.set();
paths.forEach(function(path) {
path.nodes.forEach(function(nid) {
nodesOnPath.add(nid);
});
});
}
// fix nodes not on paths
Object.keys(_nodes).forEach(function(key) {
if(_options.fixOffPathNodes && !nodesOnPath.has(key)) {
_nodes[key].fx = _nodes[key].x;
_nodes[key].fy = _nodes[key].y;
} else {
_nodes[key].fx = null;
_nodes[key].fy = null;
}
});
_simulation.force('charge').strength(_options.chargeForce);
_simulation.force('straighten', d3v4.forceStraightenPaths()
.id(function(n) { return n.dcg_nodeKey; })
.angleForce(_options.angleForce)
.pathNodes(function(p) { return p.nodes; })
.pathStrength(function(p) { return p.strength; })
.paths(paths));
}
};
function runSimulation(iterations) {
_simulation.alpha(1);
for (var i = 0; i < iterations; ++i) {
_simulation.tick();
dispatchState('tick');
}
dispatchState('end');
}
var graphviz = dc_graph.graphviz_attrs(), graphviz_keys = Object.keys(graphviz);
var engine = Object.assign(graphviz, {
layoutAlgorithm: function() {
return 'd3v4-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', 'collisionRadius',
'initialCharge', 'fixOffPathNodes']
.concat(graphviz_keys);
},
iterations: property(300),
angleForce: property(0.01),
chargeForce: property(-600),
gravityStrength: property(0.3),
collisionRadius: property(8),
initialCharge: property(-100),
fixOffPathNodes: property(false),
populateLayoutNode: function() {},
populateLayoutEdge: function() {}
});
engine.pathStraightenForce = engine.angleForce;
return engine;
};
dc_graph.d3v4_force_layout.scripts = ['d3.js', 'd3v4-force.js'];