dc.graph
Version:
Graph visualizations integrated with crossfilter and dc.js
1,635 lines (1,515 loc) • 578 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.
*
*/
(function() { function _dc_graph(d3, crossfilter, dc) {
'use strict';
/**
* 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};
}
}
function property_if(pred, curr) {
return function(o, last) {
return pred(o) ? curr(o) : last();
};
}
function property_interpolate(value, curr) {
return function(o, last) {
return d3.interpolate(last(o), curr(o))(value(o));
};
}
function multiply_properties(pred, props, blend) {
var props2 = {};
for(var p in props)
props2[p] = blend(pred, param(props[p]));
return props2;
}
function conditional_properties(pred, props) {
return multiply_properties(pred, props, property_if);
}
function node_edge_conditions(npred, epred, props) {
var nprops = {}, eprops = {}, badprops = [];
for(var p in props) {
if(/^node/.test(p))
nprops[p] = props[p];
else if(/^edge/.test(p))
eprops[p] = props[p];
else badprops.push(p);
}
if(badprops.length)
console.error('only know how to deal with properties that start with "node" or "edge"', badprops);
var props2 = npred ? conditional_properties(npred, nprops) : {};
if(epred)
Object.assign(props2, conditional_properties(epred, eprops));
return props2;
}
function cascade(parent) {
return function(level, add, props) {
for(var p in props) {
if(!parent[p])
throw new Error('unknown attribute ' + p);
parent[p].cascade(level, add ? props[p] : null);
}
return parent;
};
}
function compose(f, g) {
return function() {
return f(g.apply(null, arguments));
};
}
// version of d3.functor that optionally wraps the function with another
// one, if the parameter is a function
dc_graph.functor_wrap = function (v, wrap) {
if(typeof v === "function") {
return wrap ? function(x) {
return v(wrap(x));
} : v;
}
else return function() {
return v;
};
};
// we want to allow either values or functions to be passed to specify parameters.
// if a function, the function needs a preprocessor to extract the original key/value
// pair from the wrapper object we put it in.
function param(v) {
return dc_graph.functor_wrap(v, get_original);
}
// http://jsperf.com/cloning-an-object/101
function clone(obj) {
var target = {};
for(var i in obj) {
if(obj.hasOwnProperty(i)) {
target[i] = obj[i];
}
}
return target;
}
// because i don't think we need to bind edge point data (yet!)
var bez_cmds = {
1: 'L', 2: 'Q', 3: 'C'
};
function generate_path(pts, bezDegree, close) {
var cats = ['M', pts[0].x, ',', pts[0].y], remain = bezDegree;
var hasNaN = false;
for(var i = 1; i < pts.length; ++i) {
if(isNaN(pts[i].x) || isNaN(pts[i].y))
hasNaN = true;
cats.push(remain===bezDegree ? bez_cmds[bezDegree] : ' ', pts[i].x, ',', pts[i].y);
if(--remain===0)
remain = bezDegree;
}
if(remain!=bezDegree)
console.log("warning: pts.length didn't match bezian degree", pts, bezDegree);
if(close)
cats.push('Z');
return cats.join('');
}
// for IE (do we care really?)
Math.hypot = Math.hypot || function() {
var y = 0;
var length = arguments.length;
for (var i = 0; i < length; i++) {
if (arguments[i] === Infinity || arguments[i] === -Infinity) {
return Infinity;
}
y += arguments[i] * arguments[i];
}
return Math.sqrt(y);
};
// outputs the array with adjacent identical lines collapsed to one
function uniq(a) {
var ret = [];
a.forEach(function(x, i) {
if(i === 0 || x !== a[i-1])
ret.push(x);
});
return ret;
}
// https://tc39.github.io/ecma262/#sec-array.prototype.find
if (!Array.prototype.find) {
Object.defineProperty(Array.prototype, 'find', {
value: function(predicate) {
// 1. Let O be ? ToObject(this value).
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If IsCallable(predicate) is false, throw a TypeError exception.
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
var thisArg = arguments[1];
// 5. Let k be 0.
var k = 0;
// 6. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kValue be ? Get(O, Pk).
// c. Let testResult be ToBoolean(? Call(predicate, T, << kValue, k, O >>)).
// d. If testResult is true, return kValue.
var kValue = o[k];
if (predicate.call(thisArg, kValue, k, o)) {
return kValue;
}
// e. Increase k by 1.
k++;
}
// 7. Return undefined.
return undefined;
}
});
}
var script_path = function() {
var _path;
return function() {
if(_path === undefined) {
// adapted from http://stackoverflow.com/a/18283141/676195
_path = null; // only try once
var filename = 'dc.graph.js';
var scripts = document.getElementsByTagName('script');
if (scripts && scripts.length > 0) {
for (var i in scripts) {
if (scripts[i].src && scripts[i].src.match(new RegExp(filename+'$'))) {
_path = scripts[i].src.replace(new RegExp('(.*)'+filename+'$'), '$1');
break;
}
}
}
}
return _path;
};
}();
dc_graph.event_coords = function(diagram) {
var bound = diagram.root().node().getBoundingClientRect();
return diagram.invertCoord([d3.event.clientX - bound.left,
d3.event.clientY - bound.top]);
};
function promise_identity(x) {
return Promise.resolve(x);
}
// http://stackoverflow.com/questions/7044944/jquery-javascript-to-detect-os-without-a-plugin
var is_a_mac = navigator.platform.toUpperCase().indexOf('MAC')!==-1;
// https://stackoverflow.com/questions/16863917/check-if-class-exists-somewhere-in-parent-vanilla-js
function ancestor_has_class(element, classname) {
if(d3.select(element).classed(classname))
return true;
return element.parentElement && ancestor_has_class(element.parentElement, classname);
}
if (typeof SVGElement.prototype.contains == 'undefined') {
SVGElement.prototype.contains = HTMLDivElement.prototype.contains;
}
// arguably depth first search is a stupid algorithm to modularize -
// there are many, many interesting moments to insert a behavior
// and those end up being almost bigger than the function itself
// this is an argument for providing a graph API which could make it
// easy to just write a recursive function instead of using this
dc_graph.depth_first_traversal = function(callbacks) { // {[init, root, row, tree, place, sib, push, pop, skip,] finish, nodeid, sourceid, targetid}
return function(nodes, edges) {
callbacks.init && callbacks.init();
if(callbacks.tree)
edges = edges.filter(function(e) { return callbacks.tree(e); });
var indegree = {};
var outmap = edges.reduce(function(m, e) {
var tail = callbacks.sourceid(e),
head = callbacks.targetid(e);
if(!m[tail]) m[tail] = [];
m[tail].push(e);
indegree[head] = (indegree[head] || 0) + 1;
return m;
}, {});
var nmap = nodes.reduce(function(m, n) {
var key = callbacks.nodeid(n);
m[key] = n;
return m;
}, {});
var rows = [];
var placed = {};
function place_tree(n, r) {
var key = callbacks.nodeid(n);
if(placed[key]) {
callbacks.skip && callbacks.skip(n, indegree[key]);
return;
}
if(!rows[r])
rows[r] = [];
callbacks.place && callbacks.place(n, r, rows[r]);
rows[r].push(n);
placed[key] = true;
if(outmap[key])
outmap[key].forEach(function(e, ei) {
var target = nmap[callbacks.targetid(e)];
if(ei && callbacks.sib)
callbacks.sib(false, nmap[callbacks.targetid(outmap[key][ei-1])], target);
callbacks.push && callbacks.push();
place_tree(target, r+1);
});
callbacks.pop && callbacks.pop(n);
}
var roots;
if(callbacks.root)
roots = nodes.filter(function(n) { return callbacks.root(n); });
else {
roots = nodes.filter(function(n) { return !indegree[callbacks.nodeid(n)]; });
if(nodes.length && !roots.length) // all nodes are in a cycle
roots = [nodes[0]];
}
roots.forEach(function(n, ni) {
if(ni && callbacks.sib)
callbacks.sib(true, roots[ni-1], n);
callbacks.push && callbacks.push();
place_tree(n, callbacks.row && callbacks.row(n) || 0);
});
callbacks.finish(rows);
};
};
// basically, see if it's any simpler if we start from scratch
// (well, of course it's simpler because we have less callbacks)
// same caveats as above
dc_graph.undirected_dfs = function(callbacks) { // {[comp, node], nodeid, sourceid, targetid}
return function(nodes, edges) {
var adjacencies = edges.reduce(function(m, e) {
var tail = callbacks.sourceid(e),
head = callbacks.targetid(e);
if(!m[tail]) m[tail] = [];
if(!m[head]) m[head] = [];
m[tail].push(head);
m[head].push(tail);
return m;
}, {});
var nmap = nodes.reduce(function(m, n) {
var key = callbacks.nodeid(n);
m[key] = n;
return m;
}, {});
var found = {};
function recurse(n) {
var nid = callbacks.nodeid(n);
callbacks.node(compid, n);
found[nid] = true;
if(adjacencies[nid])
adjacencies[nid].forEach(function(adj) {
if(!found[adj])
recurse(nmap[adj]);
});
}
var compid = 0;
nodes.forEach(function(n) {
if(!found[callbacks.nodeid(n)]) {
callbacks.comp && callbacks.comp(compid);
recurse(n);
++compid;
}
});
};
};
// 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;
}
function point_on_ellipse(A, B, dx, dy) {
var tansq = Math.tan(Math.atan2(dy, dx));
tansq = tansq*tansq; // why is this not just dy*dy/dx*dx ? ?
var ret = {x: A*B/Math.sqrt(B*B + A*A*tansq), y: A*B/Math.sqrt(A*A + B*B/tansq)};
if(dx<0)
ret.x = -ret.x;
if(dy<0)
ret.y = -ret.y;
return ret;
}
var eps = 0.0000001;
function between(a, b, c) {
return a-eps <= b && b <= c+eps;
}
// Adapted from http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect/1968345#1968345
function segment_intersection(x1,y1,x2,y2, x3,y3,x4,y4) {
var x=((x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4)) /
((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4));
var y=((x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4)) /
((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4));
if (isNaN(x)||isNaN(y)) {
return false;
} else {
if (x1>=x2) {
if (!between(x2, x, x1)) {return false;}
} else {
if (!between(x1, x, x2)) {return false;}
}
if (y1>=y2) {
if (!between(y2, y, y1)) {return false;}
} else {
if (!between(y1, y, y2)) {return false;}
}
if (x3>=x4) {
if (!between(x4, x, x3)) {return false;}
} else {
if (!between(x3, x, x4)) {return false;}
}
if (y3>=y4) {
if (!between(y4, y, y3)) {return false;}
} else {
if (!between(y3, y, y4)) {return false;}
}
}
return {x: x, y: y};
}
function point_on_polygon(points, x0, y0, x1, y1) {
for(var i = 0; i < points.length; ++i) {
var next = i===points.length-1 ? 0 : i+1;
var isect = segment_intersection(points[i].x, points[i].y, points[next].x, points[next].y,
x0, y0, x1, y1);
if(isect)
return isect;
}
return null;
}
// as many as we can get from
// http://www.graphviz.org/doc/info/shapes.html
dc_graph.shape_presets = {
egg: {
// not really: an ovoid should be two half-ellipses stuck together
// https://en.wikipedia.org/wiki/Oval
generator: 'polygon',
preset: function() {
return {sides: 100, distortion: -0.25};
}
},
triangle: {
generator: 'polygon',
preset: function() {
return {sides: 3};
}
},
rectangle: {
generator: 'polygon',
preset: function() {
return {sides: 4};
}
},
diamond: {
generator: 'polygon',
preset: function() {
return {sides: 4, rotation: 45};
}
},
trapezium: {
generator: 'polygon',
preset: function() {
return {sides: 4, distortion: -0.5};
}
},
parallelogram: {
generator: 'polygon',
preset: function() {
return {sides: 4, skew: 0.5};
}
},
pentagon: {
generator: 'polygon',
preset: function() {
return {sides: 5};
}
},
hexagon: {
generator: 'polygon',
preset: function() {
return {sides: 6};
}
},
septagon: {
generator: 'polygon',
preset: function() {
return {sides: 7};
}
},
octagon: {
generator: 'polygon',
preset: function() {
return {sides: 8};
}
},
invtriangle: {
generator: 'polygon',
preset: function() {
return {sides: 3, rotation: 180};
}
},
invtrapezium: {
generator: 'polygon',
preset: function() {
return {sides: 4, distortion: 0.5};
}
},
square: {
generator: 'polygon',
preset: function() {
return {
sides: 4,
regular: true
};
}
},
plain: {
generator: 'rounded-rect',
preset: function() {
return {
noshape: true
};
}
},
house: {
generator: 'elaborated-rect',
preset: function() {
return {
get_points: function(rx, ry) {
return [
{x: rx, y: ry*2/3},
{x: rx, y: -ry/2},
{x: 0, y: -ry},
{x: -rx, y: -ry/2},
{x: -rx, y: ry*2/3}
];
},
minrx: 30
};
}
},
invhouse: {
generator: 'elaborated-rect',
preset: function() {
return {
get_points: function(rx, ry) {
return [
{x: rx, y: ry/2},
{x: rx, y: -ry*2/3},
{x: -rx, y: -ry*2/3},
{x: -rx, y: ry/2},
{x: 0, y: ry}
];
},
minrx: 30
};
}
},
rarrow: {
generator: 'elaborated-rect',
preset: function() {
return {
get_points: function(rx, ry) {
return [
{x: rx, y: ry},
{x: rx, y: ry*1.5},
{x: rx + ry*1.5, y: 0},
{x: rx, y: -ry*1.5},
{x: rx, y: -ry},
{x: -rx, y: -ry},
{x: -rx, y: ry}
];
},
minrx: 30
};
}
},
larrow: {
generator: 'elaborated-rect',
preset: function() {
return {
get_points: function(rx, ry) {
return [
{x: -rx, y: ry},
{x: -rx, y: ry*1.5},
{x: -rx - ry*1.5, y: 0},
{x: -rx, y: -ry*1.5},
{x: -rx, y: -ry},
{x: rx, y: -ry},
{x: rx, y: ry}
];
},
minrx: 30
};
}
},
rpromoter: {
generator: 'elaborated-rect',
preset: function() {
return {
get_points: function(rx, ry) {
return [
{x: rx, y: ry},
{x: rx, y: ry*1.5},
{x: rx + ry*1.5, y: 0},
{x: rx, y: -ry*1.5},
{x: rx, y: -ry},
{x: -rx, y: -ry},
{x: -rx, y: ry*1.5},
{x: 0, y: ry*1.5},
{x: 0, y: ry},
];
},
minrx: 30
};
}
},
lpromoter: {
generator: 'elaborated-rect',
preset: function() {
return {
get_points: function(rx, ry) {
return [
{x: -rx, y: ry},
{x: -rx, y: ry*1.5},
{x: -rx - ry*1.5, y: 0},
{x: -rx, y: -ry*1.5},
{x: -rx, y: -ry},
{x: rx, y: -ry},
{x: rx, y: ry*1.5},
{x: 0, y: ry*1.5},
{x: 0, y: ry}
];
},
minrx: 30
};
}
},
cds: {
generator: 'elaborated-rect',
preset: function() {
return {
get_points: function(rx, ry) {
return [
{x: rx, y: ry},
{x: rx + ry, y: 0},
{x: rx, y: -ry},
{x: -rx, y: -ry},
{x: -rx, y: ry}
];
},
minrx: 30
};
}
},
};
dc_graph.shape_presets.box = dc_graph.shape_presets.rect = dc_graph.shape_presets.rectangle;
dc_graph.available_shapes = function() {
var shapes = Object.keys(dc_graph.shape_presets);
return shapes.slice(0, shapes.length-1); // not including polygon
};
var default_shape = {shape: 'ellipse'};
function normalize_shape_def(diagram, n) {
var def = diagram.nodeShape.eval(n);
if(!def)
return default_shape;
if(typeof def === 'string')
return {shape: def};
return def;
}
function elaborate_shape(diagram, def) {
var shape = def.shape, def2 = Object.assign({}, def);
delete def2.shape;
if(shape === 'random') {
var available = dc_graph.available_shapes(); // could include diagram.shape !== ellipse, polygon
shape = available[Math.floor(Math.random()*available.length)];
}
else if(diagram.shape.enum().indexOf(shape) !== -1)
return diagram.shape(shape).elaborate({shape: shape}, def2);
if(!dc_graph.shape_presets[shape]) {
console.warn('unknown shape ', shape);
return default_shape;
}
var preset = dc_graph.shape_presets[shape].preset(def2);
preset.shape = dc_graph.shape_presets[shape].generator;
return diagram.shape(preset.shape).elaborate(preset, def2);
}
function infer_shape(diagram) {
return function(n) {
var def = normalize_shape_def(diagram, n);
n.dcg_shape = elaborate_shape(diagram, def);
n.dcg_shape.abstract = def;
};
}
function shape_changed(diagram) {
return function(n) {
var def = normalize_shape_def(diagram, n);
var old = n.dcg_shape.abstract;
if(def.shape !== old.shape)
return true;
else if(def.shape === 'polygon') {
return def.shape.sides !== old.sides || def.shape.skew !== old.skew ||
def.shape.distortion !== old.distortion || def.shape.rotation !== old.rotation;
}
else return false;
};
}
function node_label_padding(diagram, n) {
var nlp = diagram.nodeLabelPadding.eval(n);
if(typeof nlp === 'number' || typeof nlp === 'string')
return {x: +nlp, y: +nlp};
else return nlp;
}
function fit_shape(shape, diagram) {
return function(content) {
content.each(function(n) {
var bbox = null;
if((!shape.useTextSize || shape.useTextSize(n.dcg_shape)) && diagram.nodeFitLabel.eval(n)) {
bbox = getBBoxNoThrow(this);
bbox = {x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height};
var padding;
var content = diagram.nodeContent.eval(n);
if(content && diagram.content(content).padding)
padding = diagram.content(content).padding(n);
else {
var padding2 = node_label_padding(diagram, n);
padding = {
x: padding2.x*2,
y: padding2.y*2
};
}
bbox.width += padding.x;
bbox.height += padding.y;
n.bbox = bbox;
}
var r = 0, radii;
if(!shape.useRadius || shape.useRadius(n.dcg_shape))
r = diagram.nodeRadius.eval(n);
if(bbox && bbox.width && bbox.height || shape.useTextSize && !shape.useTextSize(n.dcg_shape))
radii = shape.calc_radii(n, r, bbox);
else
radii = {rx: r, ry: r};
n.dcg_rx = radii.rx;
n.dcg_ry = radii.ry;
var w = radii.rx*2, h = radii.ry*2;
// fixme: this is only consistent if regular || !squeeze
// but we'd need to calculate polygon first in order to find out
// (not a bad idea, just no time right now)
// if(w<h) w = h;
if(!shape.usePaddingAndStroke || shape.usePaddingAndStroke(n.dcg_shape)) {
var pands = diagram.nodePadding.eval(n) + diagram.nodeStrokeWidth.eval(n);
w += pands;
h += pands;
}
n.cola.width = w;
n.cola.height = h;
});
};
}
function ellipse_attrs(diagram) {
return {
rx: function(n) { return n.dcg_rx; },
ry: function(n) { return n.dcg_ry; }
};
}
function polygon_attrs(diagram, n) {
return {
d: function(n) {
var rx = n.dcg_rx, ry = n.dcg_ry,
def = n.dcg_shape,
sides = def.sides || 4,
skew = def.skew || 0,
distortion = def.distortion || 0,
rotation = def.rotation || 0,
align = (sides%2 ? 0 : 0.5), // even-sided horizontal top, odd pointy top
angles = [];
rotation = rotation/360 + 0.25; // start at y axis not x
for(var i = 0; i<sides; ++i) {
var theta = -((i+align)/sides + rotation)*Math.PI*2; // svg is up-negative
angles.push({x: Math.cos(theta), y: Math.sin(theta)});
}
var yext = d3.extent(angles, function(theta) { return theta.y; });
if(def.regular)
rx = ry = Math.max(rx, ry);
else if(rx < ry && !def.squeeze)
rx = ry;
else
ry = ry / Math.min(-yext[0], yext[1]);
n.dcg_points = angles.map(function(theta) {
var x = rx*theta.x,
y = ry*theta.y;
x *= 1 + distortion*((ry-y)/ry - 1);
x -= skew*y/2;
return {x: x, y: y};
});
return generate_path(n.dcg_points, 1, true);
}
};
}
function binary_search(f, a, b) {
var patience = 100;
if(f(a).val >= 0)
throw new Error("f(a) must be less than 0");
if(f(b).val <= 0)
throw new Error("f(b) must be greater than 0");
while(true) {
if(!--patience)
throw new Error("patience ran out");
var c = (a+b)/2,
f_c = f(c), fv = f_c.val;
if(Math.abs(fv) < 0.5)
return f_c;
if(fv > 0)
b = c;
else
a = c;
}
}
function draw_edge_to_shapes(diagram, e, sx, sy, tx, ty,
neighbor, dir, offset, source_padding, target_padding) {
var deltaX, deltaY,
sp, tp, points, bezDegree,
headAng, retPath;
if(!neighbor) {
sp = e.sourcePort.pos;
tp = e.targetPort.pos;
if(!sp) sp = {x: 0, y: 0};
if(!tp) tp = {x: 0, y: 0};
points = [{
x: sx + sp.x,
y: sy + sp.y
}, {
x: tx + tp.x,
y: ty + tp.y
}];
bezDegree = 1;
}
else {
var p_on_s = function(node, ang) {
return diagram.shape(node.dcg_shape.shape).intersect_vec(node, Math.cos(ang)*1000, Math.sin(ang)*1000);
};
var compare_dist = function(node, port0, goal) {
return function(ang) {
var port = p_on_s(node, ang);
if(!port)
return {
port: {x: 0, y: 0},
val: 0,
ang: ang
};
else
return {
port: port,
val: Math.hypot(port.x - port0.x, port.y - port0.y) - goal,
ang: ang
};
};
};
var srcang = Math.atan2(neighbor.sourcePort.y, neighbor.sourcePort.x),
tarang = Math.atan2(neighbor.targetPort.y, neighbor.targetPort.x);
var bss, bst;
// don't like this but throwing is unacceptable
try {
bss = binary_search(compare_dist(e.source, neighbor.sourcePort, offset),
srcang, srcang + 2 * dir * offset / source_padding);
}
catch(x) {
bss = {ang: srcang, port: neighbor.sourcePort};
}
try {
bst = binary_search(compare_dist(e.target, neighbor.targetPort, offset),
tarang, tarang - 2 * dir * offset / source_padding);
}
catch(x) {
bst = {ang: tarang, port: neighbor.targetPort};
}
sp = bss.port;
tp = bst.port;
var sdist = Math.hypot(sp.x, sp.y),
tdist = Math.hypot(tp.x, tp.y),
c1dist = sdist+source_padding/2,
c2dist = tdist+target_padding/2;
var c1X = sx + c1dist * Math.cos(bss.ang),
c1Y = sy + c1dist * Math.sin(bss.ang),
c2X = tx + c2dist * Math.cos(bst.ang),
c2Y = ty + c2dist * Math.sin(bst.ang);
points = [
{x: sx + sp.x, y: sy + sp.y},
{x: c1X, y: c1Y},
{x: c2X, y: c2Y},
{x: tx + tp.x, y: ty + tp.y}
];
bezDegree = 3;
}
return {
sourcePort: sp,
targetPort: tp,
points: points,
bezDegree: bezDegree
};
}
function is_one_segment(path) {
return path.bezDegree === 1 && path.points.length === 2 ||
path.bezDegree === 3 && path.points.length === 4;
}
function as_bezier3(path) {
var p = path.points;
if(path.bezDegree === 3) return p;
else if(path.bezDegree === 1)
return [
{
x: p[0].x,
y: p[0].y
},
{
x: p[0].x + (p[1].x - p[0].x)/3,
y: p[0].y + (p[1].y - p[0].y)/3
},
{
x: p[0].x + 2*(p[1].x - p[0].x)/3,
y: p[0].y + 2*(p[1].y - p[0].y)/3
},
{
x: p[1].x,
y: p[1].y
}
];
else throw new Error('unknown bezDegree ' + path.bezDegree);
}
// from https://www.jasondavies.com/animated-bezier/
function interpolate(d, p) {
var r = [];
for (var i=1; i<d.length; i++) {
var d0 = d[i-1], d1 = d[i];
r.push({x: d0.x + (d1.x - d0.x) * p, y: d0.y + (d1.y - d0.y) * p});
}
return r;
}
function getLevels(points, t_) {
var x = [points];
for (var i=1; i<points.length; i++) {
x.push(interpolate(x[x.length-1], t_));
}
return x;
}
// get a point on a bezier segment, where 0 <= t <= 1
function bezier_point(points, t_) {
var q = getLevels(points, t_);
return q[q.length-1][0];
}
// from https://stackoverflow.com/questions/8369488/splitting-a-bezier-curve#8405756
// somewhat redundant with the above but different objective
function split_bezier(p, t) {
var x1 = p[0].x, y1 = p[0].y,
x2 = p[1].x, y2 = p[1].y,
x3 = p[2].x, y3 = p[2].y,
x4 = p[3].x, y4 = p[3].y,
x12 = (x2-x1)*t+x1,
y12 = (y2-y1)*t+y1,
x23 = (x3-x2)*t+x2,
y23 = (y3-y2)*t+y2,
x34 = (x4-x3)*t+x3,
y34 = (y4-y3)*t+y3,
x123 = (x23-x12)*t+x12,
y123 = (y23-y12)*t+y12,
x234 = (x34-x23)*t+x23,
y234 = (y34-y23)*t+y23,
x1234 = (x234-x123)*t+x123,
y1234 = (y234-y123)*t+y123;
return [
[{x: x1, y: y1}, {x: x12, y: y12}, {x: x123, y: y123}, {x: x1234, y: y1234}],
[{x: x1234, y: y1234}, {x: x234, y: y234}, {x: x34, y: y34}, {x: x4, y: y4}]
];
}
function split_bezier_n(p, n) {
var ret = [];
while(n > 1) {
var parts = split_bezier(p, 1/n);
ret.push(parts[0][0], parts[0][1], parts[0][2]);
p = parts[1];
--n;
}
ret.push.apply(ret, p);
return ret;
}
// binary search for a point along a bezier that is a certain distance from one of the end points
// return the bezier cut at that point.
function chop_bezier(points, end, dist) {
var EPS = 0.1, dist2 = dist*dist;
var ref, dir, segment;
if(end === 'head') {
ref = points[points.length-1];
segment = points.slice(points.length-4);
dir = -1;
} else {
ref = points[0];
segment = points.slice(0, 4);
dir = 1;
}
var parts, d2, t = 0.5, dt = 0.5, dx, dy;
do {
parts = split_bezier(segment, t);
dx = ref.x - parts[1][0].x;
dy = ref.y - parts[1][0].y;
d2 = dx*dx + dy*dy;
dt /= 2;
if(d2 > dist2)
t -= dt*dir;
else
t += dt*dir;
//console.log('dist', dist, 'dir', dir, 'd', d, 't', t, 'dt', dt);
}
while(dt > 0.0000001 && Math.abs(d2 - dist2) > EPS);
points = points.slice();
if(end === 'head')
return points.slice(0, points.length-4).concat(parts[0]);
else
return parts[1].concat(points.slice(4));
}
function angle_between_points(p0, p1) {
return Math.atan2(p1.y - p0.y, p1.x - p0.x);
}
dc_graph.no_shape = function() {
var _shape = {
parent: property(null),
elaborate: function(preset, def) {
return Object.assign(preset, def);
},
useTextSize: function() { return false; },
useRadius: function() { return false; },
usePaddingAndStroke: function() { return false; },
intersect_vec: function(n, deltaX, deltaY) {
return {x: 0, y: 0};
},
calc_radii: function(n, ry, bbox) {
return {rx: 0, ry: 0};
},
create: function(nodeEnter) {
},
replace: function(nodeChanged) {
},
update: function(node) {
}
};
return _shape;
};
dc_graph.ellipse_shape = function() {
var _shape = {
parent: property(null),
elaborate: function(preset, def) {
return Object.assign(preset, def);
},
intersect_vec: function(n, deltaX, deltaY) {
return point_on_ellipse(n.dcg_rx, n.dcg_ry, deltaX, deltaY);
},
calc_radii: function(n, ry, bbox) {
// make sure we can fit height in r
ry = Math.max(ry, bbox.height/2 + 5);
var rx = bbox.width/2;
// solve (x/A)^2 + (y/B)^2) = 1 for A, with B=r, to fit text in ellipse
// http://stackoverflow.com/a/433438/676195
var y_over_B = bbox.height/2/ry;
rx = rx/Math.sqrt(1 - y_over_B*y_over_B);
rx = Math.max(rx, ry);
return {rx: rx, ry: ry};
},
create: function(nodeEnter) {
nodeEnter.insert('ellipse', ':first-child')
.attr('class', 'node-shape');
},
update: function(node) {
node.select('ellipse.node-shape')
.attr(ellipse_attrs(_shape.parent()));
}
};
return _shape;
};
dc_graph.polygon_shape = function() {
var _shape = {
parent: property(null),
elaborate: function(preset, def) {
return Object.assign(preset, def);
},
intersect_vec: function(n, deltaX, deltaY) {
return point_on_polygon(n.dcg_points, 0, 0, deltaX, deltaY);
},
calc_radii: function(n, ry, bbox) {
// make sure we can fit height in r
ry = Math.max(ry, bbox.height/2 + 5);
var rx = bbox.width/2;
// this is cribbed from graphviz but there is much i don't understand
// and any errors are mine
// https://github.com/ellson/graphviz/blob/6acd566eab716c899ef3c4ddc87eceb9b428b627/lib/common/shapes.c#L1996
rx = rx*Math.sqrt(2)/Math.cos(Math.PI/(n.dcg_shape.sides||4));
return {rx: rx, ry: ry};
},
create: function(nodeEnter) {
nodeEnter.insert('path', ':first-child')
.attr('class', 'node-shape');
},
update: function(node) {
node.select('path.node-shape')
.attr(polygon_attrs(_shape.parent()));
}
};
return _shape;
};
dc_graph.rounded_rectangle_shape = function() {
var _shape = {
parent: property(null),
elaborate: function(preset, def) {
preset = Object.assign({rx: 10, ry: 10}, preset);
return Object.assign(preset, def);
},
intersect_vec: function(n, deltaX, deltaY) {
var points = [
{x: n.dcg_rx, y: n.dcg_ry},
{x: n.dcg_rx, y: -n.dcg_ry},
{x: -n.dcg_rx, y: -n.dcg_ry},
{x: -n.dcg_rx, y: n.dcg_ry}
];
return point_on_polygon(points, 0, 0, deltaX, deltaY); // not rounded
},
useRadius: function(shape) {
return !shape.noshape;
},
calc_radii: function(n, ry, bbox) {
var fity = bbox.height/2;
// fixme: fudge to make sure text is not too tall for node
if(!n.dcg_shape.noshape)
fity += 5;
return {
rx: bbox.width / 2,
ry: Math.max(ry, fity)
};
},
create: function(nodeEnter) {
nodeEnter.filter(function(n) {
return !n.dcg_shape.noshape;
}).insert('rect', ':first-child')
.attr('class', 'node-shape');
},
update: function(node) {
node.select('rect.node-shape')
.attr({
x: function(n) {
return -n.dcg_rx;
},
y: function(n) {
return -n.dcg_ry;
},
width: function(n) {
return 2*n.dcg_rx;
},
height: function(n) {
return 2*n.dcg_ry;
},
rx: function(n) {
return n.dcg_shape.rx + 'px';
},
ry: function(n) {
return n.dcg_shape.ry + 'px';
}
});
}
};
return _shape;
};
// this is not all that accurate - idea is that arrows, houses, etc, are rectangles
// in terms of sizing, but elaborated drawing & clipping. refine until done.
dc_graph.elaborated_rectangle_shape = function() {
var _shape = dc_graph.rounded_rectangle_shape();
_shape.intersect_vec = function(n, deltaX, deltaY) {
var points = n.dcg_shape.get_points(n.dcg_rx, n.dcg_ry);
return point_on_polygon(points, 0, 0, deltaX, deltaY);
};
delete _shape.useRadius;
var orig_radii = _shape.calc_radii;
_shape.calc_radii = function(n, ry, bbox) {
var ret = orig_radii(n, ry, bbox);
return {
rx: Math.max(ret.rx, n.dcg_shape.minrx),
ry: ret.ry
};
};
_shape.create = function(nodeEnter) {
nodeEnter.insert('path', ':first-child')
.attr('class', 'node-shape');
};
_shape.update = function(node) {
node.select('path.node-shape')
.attr('d', function(n) {
return generate_path(n.dcg_shape.get_points(n.dcg_rx, n.dcg_ry), 1, true);
});
};
return _shape;
};
function offsetx(ofsx) {
return function(p) {
return {x: p.x + ofsx, y: p.y};
};
}
dc_graph.builtin_arrows = {
box: function(open, side) {
if(!open) return {
frontRef: [8,0],
drawFunction: function(marker, ofs, stemWidth) {
marker.append('rect')
.attr({
x: ofs[0],
y: side==='right' ? -stemWidth/2 : -4,
width: 8,
height: side ? 4+stemWidth/2 : 8,
'stroke-width': 0
});
}
};
else return {
frontRef: [8,0],
drawFunction: function(marker, ofs, stemWidth) {
marker.append('rect')
.attr({
x: ofs[0] + 0.5,
y: side==='right' ? 0 : -3.5,
width: 7,
height: side ? 3.5 : 7,
'stroke-width': 1,
fill: 'none'
});
if(side)
marker.append('svg:path')
.attr({
d: ['M', ofs[0], 0, 'h',8].join(' '),
'stroke-width': stemWidth,
fill: 'none'
});
}
};
},
curve: function(open, side) {
return {
stems: [true,false],
kernstems: [0, 0.25],
frontRef: [8,0],
drawFunction: function(marker, ofs, stemWidth) {
var instrs = [];
instrs.pus