dc.graph
Version:
Graph visualizations integrated with crossfilter and dc.js
887 lines (812 loc) • 28.1 kB
JavaScript
/*!
* dc.graph 0.9.8
* http://dc-js.github.io/dc.graph.js/
* Copyright 2015-2019 AT&T Intellectual Property & the dc.graph.js Developers
* https://github.com/dc-js/dc.graph.js/blob/master/AUTHORS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/**
* The entire dc.graph.js library is scoped under the **dc_graph** name space. It does not introduce
* anything else into the global name space.
*
* Like in dc.js and most libraries built on d3, most `dc_graph` functions are designed to allow function chaining, meaning they return the current diagram
* instance whenever it is appropriate. The getter forms of functions do not participate in function
* chaining because they return values that are not the diagram.
* @namespace dc_graph
* @version 0.9.8
* @example
* // Example chaining
* diagram.width(600)
* .height(400)
* .nodeDimension(nodeDim)
* .nodeGroup(nodeGroup);
*/
var dc_graph = {
version: '0.9.8',
constants: {
CHART_CLASS: 'dc-graph'
}
};
function get_original(x) {
return x.orig;
}
function identity(x) {
return x;
};
var property = function (defaultValue, unwrap) {
if(unwrap === undefined)
unwrap = get_original;
else if(unwrap === false)
unwrap = identity;
var value = defaultValue, react = null;
var cascade = [];
var ret = function (_) {
if (!arguments.length) {
return value;
}
if(react)
react(_);
value = _;
return this;
};
ret.cascade = function (n, f) {
for(var i = 0; i<cascade.length; ++i) {
if(cascade[i].n === n) {
if(f)
cascade[i].f = f;
else cascade.splice(i, 1);
return ret;
} else if(cascade[i].n > n) {
cascade.splice(i, 0, {n: n, f: f});
return ret;
}
}
cascade.push({n: n, f: f});
return ret;
};
ret._eval = function(o, n) {
if(n===0 || !cascade.length)
return dc_graph.functor_wrap(ret(), unwrap)(o);
else {
var last = cascade[n-1];
return last.f(o, function() {
return ret._eval(o, n-1);
});
}
};
ret.eval = function(o) {
return ret._eval(o, cascade.length);
};
ret.react = function(_) {
if (!arguments.length) {
return react;
}
react = _;
return this;
};
return ret;
};
function named_children() {
var _children = {};
var f = function(id, object) {
if(arguments.length === 1)
return _children[id];
if(f.reject) {
var reject = f.reject(id, object);
if(reject) {
console.groupCollapsed(reject);
console.trace();
console.groupEnd();
return this;
}
}
// do not notify unnecessarily
if(_children[id] === object)
return this;
if(_children[id])
_children[id].parent(null);
_children[id] = object;
if(object)
object.parent(this);
return this;
};
f.enum = function() {
return Object.keys(_children);
};
f.nameOf = function(o) {
var found = Object.entries(_children).find(function(kv) {
return kv[1] == o;
});
return found ? found[0] : null;
};
return f;
}
function deprecated_property(message, defaultValue) {
var prop = property(defaultValue);
var ret = function() {
if(arguments.length) {
console.warn(message);
prop.apply(property, arguments);
return this;
}
return prop();
};
['cascade', '_eval', 'eval', 'react'].forEach(function(method) {
ret[method] = prop[method];
});
return ret;
}
function onetime_trace(level, message) {
var said = false;
return function() {
if(said)
return;
if(level === 'trace') {
console.groupCollapsed(message);
console.trace();
console.groupEnd();
}
else
console[level](message);
said = true;
};
}
function deprecation_warning(message) {
return onetime_trace('warn', message);
}
function trace_function(level, message, f) {
var dep = onetime_trace(level, message);
return function() {
dep();
return f.apply(this, arguments);
};
}
function deprecate_function(message, f) {
return trace_function('warn', message, f);
}
// http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
function is_ie() {
var ua = window.navigator.userAgent;
return(ua.indexOf('MSIE ') > 0 ||
ua.indexOf('Trident/') > 0 ||
ua.indexOf('Edge/') > 0);
}
function is_safari() {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}
// polyfill Object.assign for IE
// it's just too useful to do without
if (typeof Object.assign != 'function') {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, "assign", {
value: function assign(target, varArgs) { // .length of function is 2
'use strict';
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) { // Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
value: function(valueToFind, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
// 1. Let O be ? ToObject(this value).
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If len is 0, return false.
if (len === 0) {
return false;
}
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
// 5. If n >= 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
function sameValueZero(x, y) {
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}
// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(valueToFind, elementK) is true, return true.
if (sameValueZero(o[k], valueToFind)) {
return true;
}
// c. Increase k by 1.
k++;
}
// 8. Return false
return false;
}
});
}
if (!Object.entries) {
Object.entries = function( obj ){
var ownProps = Object.keys( obj ),
i = ownProps.length,
resArray = new Array(i); // preallocate the Array
while (i--)
resArray[i] = [ownProps[i], obj[ownProps[i]]];
return resArray;
};
}
// https://github.com/KhaledElAnsari/Object.values
Object.values = Object.values ? Object.values : function(obj) {
var allowedTypes = ["[object String]", "[object Object]", "[object Array]", "[object Function]"];
var objType = Object.prototype.toString.call(obj);
if(obj === null || typeof obj === "undefined") {
throw new TypeError("Cannot convert undefined or null to object");
} else if(!~allowedTypes.indexOf(objType)) {
return [];
} else {
// if ES6 is supported
if (Object.keys) {
return Object.keys(obj).map(function (key) {
return obj[key];
});
}
var result = [];
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
result.push(obj[prop]);
}
}
return result;
}
};
function getBBoxNoThrow(elem) {
// firefox seems to have issues with some of my texts
// just catch for now
try {
return elem.getBBox();
} catch(xep) {
return {x: 0, y: 0, width:0, height: 0};
}
}
// create or re-use objects in a map, delete the ones that were not reused
function regenerate_objects(preserved, list, need, key, assign, create, destroy) {
if(!create) create = function(k, o) { };
if(!destroy) destroy = function(k) { };
var keep = {};
function wrap(o) {
var k = key(o);
if(!preserved[k])
create(k, preserved[k] = {}, o);
var o1 = preserved[k];
assign(o1, o);
keep[k] = true;
return o1;
}
var wlist = list.map(wrap);
if(need)
need.forEach(function(k) {
if(!preserved[k]) { // hasn't been created, needs to be
create(k, preserved[k] = {}, null);
assign(preserved[k], null);
}
if(!keep[k]) { // wasn't in list, should be
wlist.push(preserved[k]);
keep[k] = true;
}
});
// delete any objects from last round that are no longer used
for(var k in preserved)
if(!keep[k]) {
destroy(k, preserved[k]);
delete preserved[k];
}
return wlist;
}
/**
* `dc_graph.graphviz_attrs defines a basic set of attributes which layout engines should
* implement - although these are not required, they make it easier for clients and
* modes (like expand_collapse) to work with multiple layout engines.
*
* these attributes are {@link http://www.graphviz.org/doc/info/attrs.html from graphviz}
* @class graphviz_attrs
* @memberof dc_graph
* @return {Object}
**/
dc_graph.graphviz_attrs = function() {
return {
/**
* Direction to draw ranks.
* @method rankdir
* @memberof dc_graph.graphviz_attrs
* @instance
* @param {String} [rankdir='TB'] 'TB', 'LR', 'BT', or 'RL'
**/
rankdir: property('TB'),
/**
* Spacing in between nodes in the same rank.
* @method nodesep
* @memberof dc_graph.graphviz_attrs
* @instance
* @param {String} [nodesep=40]
**/
nodesep: property(40),
/**
* Spacing in between ranks.
* @method ranksep
* @memberof dc_graph.graphviz_attrs
* @instance
* @param {String} [ranksep=40]
**/
ranksep: property(40)
};
};
// graphlib-dot seems to wrap nodes in an extra {value}
// actually this is quite a common problem with generic libs
function nvalue(n) {
return n.value.value ? n.value.value : n.value;
}
// apply standard accessors to a diagram in order to style it as graphviz would
// this is a work in progress
dc_graph.apply_graphviz_accessors = function(diagram) {
diagram
.nodeLabel(function(n) {
var label = nvalue(n).label;
if(label === undefined)
label = n.key;
return label && label.split(/\n|\\n/);
})
.nodeRadius(function(n) {
// should do width & height instead, #25
return nvalue(n).radius || 25;
})
.nodeShape(function(n) { return nvalue(n).shape; })
.nodeFill(function(n) { return nvalue(n).fillcolor || 'white'; })
.nodeOpacity(function(n) {
// not standard gv
return nvalue(n).opacity || 1;
})
.nodeLabelFill(function(n) { return nvalue(n).fontcolor || 'black'; })
.nodeTitle(function(n) {
return (nvalue(n).htmltip || nvalue(n).jsontip) ? null :
nvalue(n).tooltip !== undefined ?
nvalue(n).tooltip :
diagram.nodeLabel()(n);
})
.nodeStrokeWidth(function(n) {
// it is debatable whether a point === a pixel but they are close
// https://graphicdesign.stackexchange.com/questions/199/point-vs-pixel-what-is-the-difference
var penwidth = nvalue(n).penwidth;
return penwidth !== undefined ? +penwidth : 1;
})
.edgeLabel(function(e) { return e.value.label ? e.value.label.split(/\n|\\n/) : ''; })
.edgeStroke(function(e) { return e.value.color || 'black'; })
.edgeOpacity(function(e) {
// not standard gv
return e.value.opacity || 1;
})
.edgeArrowSize(function(e) {
return e.value.arrowsize || 1;
})
// need directedness to default these correctly, see #106
.edgeArrowhead(function(e) {
var head = e.value.arrowhead;
return head !== undefined ? head : 'vee';
})
.edgeArrowtail(function(e) {
var tail = e.value.arrowtail;
return tail !== undefined ? tail : null;
})
.edgeStrokeDashArray(function(e) {
switch(e.value.style) {
case 'dotted':
return [1,5];
}
return null;
});
var draw_clusters = diagram.child('draw-clusters');
if(draw_clusters) {
draw_clusters
.clusterStroke(function(c) {
return c.value.color || 'black';
})
.clusterFill(function(c) {
return c.value.style === 'filled' ? c.value.fillcolor || c.value.color || c.value.bgcolor : null;
})
.clusterLabel(function(c) {
return c.value.label;
});
}
};
dc_graph.snapshot_graphviz = function(diagram) {
var xDomain = diagram.x().domain(), yDomain = diagram.y().domain();
return {
nodes: diagram.nodeGroup().all().map(function(n) {
return diagram.getWholeNode(n.key);
})
.filter(function(x) { return x; })
.map(function(n) {
return {
key: diagram.nodeKey.eval(n),
label: diagram.nodeLabel.eval(n),
fillcolor: diagram.nodeFillScale()(diagram.nodeFill.eval(n)),
penwidth: diagram.nodeStrokeWidth.eval(n),
// not supported as input, see dc.graph.js#25
// width: n.cola.dcg_rx*2,
// height: n.cola.dcg_ry*2,
// not graphviz attributes
// until we have w/h
radius: diagram.nodeRadius.eval(n),
// does not seem to exist in gv
opacity: diagram.nodeOpacity.eval(n),
// should be pos
x: n.cola.x,
y: n.cola.y
};
}),
edges: diagram.edgeGroup().all().map(function(e) {
return diagram.getWholeEdge(e.key);
}).map(function(e) {
return {
key: diagram.edgeKey.eval(e),
source: diagram.edgeSource.eval(e),
target: diagram.edgeTarget.eval(e),
color: diagram.edgeStroke.eval(e),
arrowsize: diagram.edgeArrowSize.eval(e),
opacity: diagram.edgeOpacity.eval(e),
// should support dir, see dc.graph.js#106
arrowhead: diagram.edgeArrowhead.eval(e),
arrowtail: diagram.edgeArrowtail.eval(e)
};
}),
bounds: {
left: xDomain[0],
top: yDomain[0],
right: xDomain[1],
bottom: yDomain[1]
}
};
};
/**
* `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'];
var _layouts;
function postResponse(event, layoutId) {
return function() {
var message = {
response: event,
layoutId: layoutId
};
message.args = Array.prototype.slice.call(arguments);
postMessage(message);
};
}
onmessage = function(e) {
var args = e.data.args;
switch(e.data.command) {
case 'init':
// find a function under dc_graph that has `scripts`
var layout_name;
for(var name in dc_graph) {
if(typeof dc_graph[name] === 'function' && dc_graph[name].scripts)
layout_name = name;
}
if(!_layouts) {
_layouts = {};
importScripts.apply(null, dc_graph[layout_name].scripts);
if(dc_graph[layout_name].optional_scripts) {
try {
importScripts.apply(null, dc_graph[layout_name].optional_scripts);
}
catch(xep) {
console.log(xep);
}
}
}
_layouts[args.layoutId] = dc_graph[layout_name]()
.on('tick', postResponse('tick', args.layoutId))
.on('start', postResponse('start', args.layoutId))
.on('end', postResponse('end', args.layoutId))
.init(args.options);
break;
case 'data':
if(_layouts)
_layouts[args.layoutId].data(args.graph, args.nodes, args.edges, args.clusters, args.constraints);
break;
case 'start':
// if(args.initialOnly) {
// if(args.showLayoutSteps)
// _tick();
// _done();
// }
// else
_layouts[args.layoutId].start();
break;
case 'stop':
if(_layouts)
_layouts[args.layoutId].stop();
break;
}
};
//# sourceMappingURL=dc.graph.d3-force.worker.js.map